lib/sql/src/file.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const builtin = @import("builtin");
4 const sys = @import("sys");
5 const lattice = @import("lattice.zig");
6 const page = @import("page.zig");
7 const pager = @import("pager.zig");
8 const publication = @import("publication.zig");
9 const trace = @import("trace.zig");
10 const tree = @import("tree.zig");
11 const wal = @import("wal.zig");
12
13 const Allocator = std.mem.Allocator;
14 pub const default_read_cache_capacity = 128;
15 /// The digest memo entries a database workspace reserves unless its limits
16 /// name another count. Zero entries disable reuse, so every stored tree
17 /// digest hashes its state again.
18 pub const default_digest_memo_entries = 16;
19 pub const read_lease_capacity_max: u8 = 32;
20 pub const default_read_lease_limit: u8 = read_lease_capacity_max;
21 pub const default_max_wal_bytes: usize = 64 * 1024 * 1024;
22 const controlled_read_chunk_bytes: usize = 1 * 1024 * 1024;
23
24 const DatabaseFileError = error{
25 CapacityOverflow,
26 InvalidDatabaseFile,
27 InvalidPageId,
28 InvalidWalLimit,
29 TransactionClosed,
30 TransactionConflict,
31 WriteTransactionOpen,
32 TransactionTooLarge,
33 UncommittedWalTail,
34 WalLimitExceeded,
35 RecoveryRequired,
36 ReadOnlyDatabase,
37 ActiveReaders,
38 InvalidReadLease,
39 InvalidReadLeaseLimit,
40 InvalidPathStorageOwner,
41 PathStorageIdentityExhausted,
42 ReadLeaseCapacityExceeded,
43 PathLoanGenerationExhausted,
44 WorkspaceBusy,
45 WorkspaceCapacityExceeded,
46 Interrupted,
47 };
48
49 pub const Error =
50 pager.Error ||
51 publication.Error ||
52 wal.Error ||
53 std.Io.File.OpenError ||
54 std.Io.File.StatError ||
55 std.Io.File.ReadPositionalError ||
56 std.Io.File.WritePositionalError ||
57 std.Io.File.SetLengthError ||
58 std.Io.File.SyncError ||
59 std.Io.Dir.ReadFileAllocError ||
60 std.Io.Dir.RenameError ||
61 std.Io.Dir.DeleteFileError ||
62 DatabaseFileError;
63
64 pub const Paths = struct {
65 database: []const u8 = "tiny.sql.db",
66 wal: []const u8 = "tiny.sql.wal",
67 };
68
69 pub const PublicationToken = publication.Token;
70 pub const artifact_path_bytes_max: usize = 1024;
71 pub const publication_artifact_path_bytes_max: usize = artifact_path_bytes_max;
72
73 pub const PublicationFootprint = struct {
74 database_bytes: u64 = 0,
75 wal_bytes: u64 = 0,
76 metadata_bytes: u64 = 0,
77
78 pub fn totalBytes(self: PublicationFootprint) u64 {
79 return self.database_bytes +| self.wal_bytes +| self.metadata_bytes;
80 }
81 };
82
83 pub const PublicationArtifactKind = enum {
84 database,
85 wal,
86 metadata,
87 };
88
89 pub const RecoverOptions = struct {
90 io: std.Io = std.Options.debug_io,
91 paths: Paths = .{},
92 header: wal.Header,
93 max_wal_bytes: usize = default_max_wal_bytes,
94 wal_capacity_bytes: ?usize = null,
95 control: wal.Control = .{},
96 };
97
98 pub const OpenOptions = struct {
99 io: std.Io = std.Options.debug_io,
100 paths: Paths = .{},
101 header: wal.Header,
102 max_wal_bytes: usize = default_max_wal_bytes,
103 wal_capacity_bytes: ?usize = null,
104 read_cache_capacity: usize = default_read_cache_capacity,
105 read_lease_limit: u8 = default_read_lease_limit,
106 write_capacity: pager.Capacity = .{},
107 publication: ?PublicationOptions = null,
108 control: wal.Control = .{},
109 };
110
111 pub const PublicationOptions = struct {
112 base_paths: Paths,
113 selected: ?publication.Token,
114 candidate: publication.Token,
115 };
116
117 pub const PublishedReadOpenOptions = struct {
118 io: std.Io = std.Options.debug_io,
119 base_paths: Paths,
120 token: publication.Token,
121 header: wal.Header,
122 max_wal_bytes: usize = default_max_wal_bytes,
123 read_cache_capacity: usize = default_read_cache_capacity,
124 read_lease_limit: u8 = default_read_lease_limit,
125 };
126
127 pub fn artifact(
128 paths: Paths,
129 index: usize,
130 buffer: *[artifact_path_bytes_max]u8,
131 ) Error!?[]const u8 {
132 const path = switch (index) {
133 0 => paths.database,
134 1 => paths.wal,
135 2 => return try walSidecarPath(buffer, paths.wal),
136 else => return try publicationArtifact(paths, index - 3, buffer),
137 };
138 if (path.len > buffer.len) return error.NameTooLong;
139 @memcpy(buffer[0..path.len], path);
140 return buffer[0..path.len];
141 }
142
143 pub fn artifactsPresent(
144 io: std.Io,
145 dir: std.Io.Dir,
146 paths: Paths,
147 ) Error!bool {
148 var buffer: [artifact_path_bytes_max]u8 = undefined;
149 var index: usize = 0;
150 while (try artifact(paths, index, &buffer)) |name| : (index += 1) {
151 _ = dir.statFile(io, name, .{}) catch |err| switch (err) {
152 error.FileNotFound => continue,
153 else => return err,
154 };
155 return true;
156 }
157 return false;
158 }
159
160 pub fn publicationArtifact(
161 base_paths: Paths,
162 index: usize,
163 buffer: *[publication_artifact_path_bytes_max]u8,
164 ) Error!?[]const u8 {
165 if (index >= 12) return null;
166 const lane = try publication.Lane.init(.{
167 .database = base_paths.database,
168 .wal = base_paths.wal,
169 }, @intCast(index / 6));
170 const name = lane.artifact(index % 6).?;
171 if (name.len > buffer.len) return error.InvalidPublication;
172 @memcpy(buffer[0..name.len], name);
173 return buffer[0..name.len];
174 }
175
176 pub fn publicationArtifactKind(
177 base_paths: Paths,
178 path: []const u8,
179 ) Error!?PublicationArtifactKind {
180 var buffer: [publication_artifact_path_bytes_max]u8 = undefined;
181 var index: usize = 0;
182 while (try publicationArtifact(base_paths, index, &buffer)) |name| : (index += 1) {
183 if (!std.mem.eql(u8, path, name)) continue;
184 return publicationArtifactKindAt(index);
185 }
186 return null;
187 }
188
189 pub fn publicationArtifactsPresent(
190 io: std.Io,
191 dir: std.Io.Dir,
192 base_paths: Paths,
193 ) Error!bool {
194 var buffer: [publication_artifact_path_bytes_max]u8 = undefined;
195 var index: usize = 0;
196 while (try publicationArtifact(base_paths, index, &buffer)) |name| : (index += 1) {
197 _ = dir.statFile(io, name, .{}) catch |err| switch (err) {
198 error.FileNotFound => continue,
199 else => return err,
200 };
201 return true;
202 }
203 return false;
204 }
205
206 pub fn publicationFootprint(
207 io: std.Io,
208 dir: std.Io.Dir,
209 base_paths: Paths,
210 ) Error!PublicationFootprint {
211 var footprint = PublicationFootprint{};
212 var buffer: [publication_artifact_path_bytes_max]u8 = undefined;
213 var index: usize = 0;
214 while (try publicationArtifact(base_paths, index, &buffer)) |name| : (index += 1) {
215 const stat = dir.statFile(io, name, .{}) catch |err| switch (err) {
216 error.FileNotFound => continue,
217 else => return err,
218 };
219 const total = switch (publicationArtifactKindAt(index)) {
220 .database => &footprint.database_bytes,
221 .wal => &footprint.wal_bytes,
222 .metadata => &footprint.metadata_bytes,
223 };
224 total.* = std.math.add(u64, total.*, stat.size) catch
225 return error.InvalidPublication;
226 }
227 return footprint;
228 }
229
230 fn publicationArtifactKindAt(index: usize) PublicationArtifactKind {
231 return switch (index % 6) {
232 0, 3 => .database,
233 1, 4 => .wal,
234 2, 5 => .metadata,
235 else => unreachable,
236 };
237 }
238
239 pub const ReadOpenOptions = struct {
240 io: std.Io = std.Options.debug_io,
241 paths: Paths = .{},
242 header: wal.Header,
243 max_wal_bytes: usize = default_max_wal_bytes,
244 read_cache_capacity: usize = default_read_cache_capacity,
245 read_lease_limit: u8 = default_read_lease_limit,
246 control: wal.Control = .{},
247 };
248
249 pub const SparseReadProbe = struct {
250 wal_reads: usize = 0,
251 wal_bytes: usize = 0,
252 base_reads: usize = 0,
253 };
254
255 pub const SparseReadOptions = struct {
256 io: std.Io = std.Options.debug_io,
257 paths: Paths = .{},
258 workspace: wal.Scanner.Workspace,
259 control: wal.Control = .{},
260 probe: ?*SparseReadProbe = null,
261 };
262
263 pub const SparseRefreshOptions = struct {
264 paths: Paths = .{},
265 control: wal.Control = .{},
266 };
267
268 const PathStorageIdentitySequence = struct {
269 next: sys.atomic.Sequence64 = .init(0),
270
271 fn take(self: *PathStorageIdentitySequence) PathStorage.IdentityError!u64 {
272 return self.next.take() orelse error.PathStorageIdentityExhausted;
273 }
274 };
275
276 var path_storage_identities = PathStorageIdentitySequence{};
277
278 /// Holds a database's path bytes inside a workspace so that an open database
279 /// can keep its own copies of the paths it was given without an allocator,
280 /// owning four fixed regions: the current database path, the current
281 /// write-ahead log path, and the pair of publication base paths. The storage
282 /// hands out one loan at a time and reports `PathStorageBusy` while a loan is
283 /// live, stamping each loan with an identity number of its own and a fresh
284 /// epoch so that a loan from an earlier acquisition is rejected. Because the
285 /// owner records its own address when it activates and checks that address on
286 /// every loan use, an activated owner that has been moved refuses every loan,
287 /// so callers leave an activated owner at one address for as long as any loan
288 /// is live and through `deinit`. `deinit` asserts that no loan is live and then
289 /// returns the caller's storage.
290 pub const PathStorage = struct {
291 pub const storage_alignment: usize = @alignOf(u8);
292 pub const Storage = []align(storage_alignment) u8;
293
294 pub const Limits = struct {
295 database_bytes: usize,
296 wal_bytes: usize,
297 publication_database_bytes: usize = 0,
298 publication_wal_bytes: usize = 0,
299
300 pub fn forDirect(paths: Paths) Limits {
301 return .{
302 .database_bytes = paths.database.len,
303 .wal_bytes = paths.wal.len,
304 };
305 }
306
307 pub fn forPublication(base_paths: Paths) error{InvalidPublication}!Limits {
308 const pair = try publicationPathBytes(base_paths);
309 return .{
310 .database_bytes = pair.database,
311 .wal_bytes = pair.wal,
312 .publication_database_bytes = base_paths.database.len,
313 .publication_wal_bytes = base_paths.wal.len,
314 };
315 }
316
317 pub fn forPublished(base_paths: Paths) error{InvalidPublication}!Limits {
318 const pair = try publicationPathBytes(base_paths);
319 return .{
320 .database_bytes = pair.database,
321 .wal_bytes = pair.wal,
322 };
323 }
324
325 pub fn forOpen(options: OpenOptions) error{InvalidPublication}!Limits {
326 const options_publication = options.publication orelse
327 return forDirect(options.paths);
328 return forPublication(options_publication.base_paths);
329 }
330 };
331
332 pub const Capacity = struct {
333 database_bytes: usize,
334 wal_bytes: usize,
335 publication_database_bytes: usize,
336 publication_wal_bytes: usize,
337 storage_bytes: usize,
338
339 pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity {
340 var storage_bytes = std.math.add(
341 usize,
342 limits.database_bytes,
343 limits.wal_bytes,
344 ) catch return error.CapacityOverflow;
345 storage_bytes = std.math.add(
346 usize,
347 storage_bytes,
348 limits.publication_database_bytes,
349 ) catch return error.CapacityOverflow;
350 storage_bytes = std.math.add(
351 usize,
352 storage_bytes,
353 limits.publication_wal_bytes,
354 ) catch return error.CapacityOverflow;
355 return .{
356 .database_bytes = limits.database_bytes,
357 .wal_bytes = limits.wal_bytes,
358 .publication_database_bytes = limits.publication_database_bytes,
359 .publication_wal_bytes = limits.publication_wal_bytes,
360 .storage_bytes = storage_bytes,
361 };
362 }
363
364 pub fn storageBytes(self: Capacity) usize {
365 return self.storage_bytes;
366 }
367 };
368
369 pub const IdentityError = error{PathStorageIdentityExhausted};
370 pub const InitError = IdentityError || error{ CapacityOverflow, StorageTooShort };
371 pub const OwnerError = error{InvalidPathStorageOwner};
372 pub const Exhaustion = error{
373 PathCapacityExceeded,
374 PathLoanGenerationExhausted,
375 PathStorageBusy,
376 };
377 pub const AcquireError = OwnerError || Exhaustion || error{CapacityOverflow};
378 pub const work_limits: alloc_phase.capacity.WorkLimits = .{
379 .transition_steps_max = std.math.maxInt(usize),
380 .cleanup_steps_per_call_max = 0,
381 .cleanup_calls_at_capacity_max = 0,
382 };
383
384 pub const claim: alloc_phase.capacity.Declaration = .{
385 .source = .{
386 .id = "sql.file_path_storage",
387 .kind = .phase_static,
388 .limit_source = .caller,
389 .storage = .{
390 .covered = &.{
391 .{
392 .id = "current_database_path_bytes",
393 .lifetime = .steady,
394 .detail = "current database path bytes retained for later file operations",
395 },
396 .{
397 .id = "current_wal_path_bytes",
398 .lifetime = .steady,
399 .detail = "current WAL path bytes retained for later file operations",
400 },
401 .{
402 .id = "publication_base_database_path_bytes",
403 .lifetime = .steady,
404 .detail = "optional publication base database path bytes",
405 },
406 .{
407 .id = "publication_base_wal_path_bytes",
408 .lifetime = .steady,
409 .detail = "optional publication base WAL path bytes",
410 },
411 },
412 .excluded = &.{
413 "caller-owned input path slices and directory handles",
414 "operating-system path state file contents and I/O runtime state",
415 "publication lane scratch names owned by sql.publication.Lane",
416 },
417 },
418 .capacity = .{
419 .inputs = &.{
420 alloc_phase.capacity.bindInput(Limits, "database_bytes", "database_bytes"),
421 alloc_phase.capacity.bindInput(Limits, "wal_bytes", "wal_bytes"),
422 alloc_phase.capacity.bindInput(
423 Limits,
424 "publication_database_bytes",
425 "publication_database_bytes",
426 ),
427 alloc_phase.capacity.bindInput(
428 Limits,
429 "publication_wal_bytes",
430 "publication_wal_bytes",
431 ),
432 },
433 .type_selectors = &.{},
434 .nodes = &.{
435 .{ .input = 0 },
436 .{ .input = 1 },
437 .{ .input = 2 },
438 .{ .input = 3 },
439 .{ .add = .{ .left = 0, .right = 1 } },
440 .{ .add = .{ .left = 4, .right = 2 } },
441 .{ .add = .{ .left = 5, .right = 3 } },
442 },
443 .assertions = &.{.{
444 .scope = .closure_total,
445 .measure = .retained,
446 .relation = .exact,
447 .expression = 6,
448 }},
449 },
450 .overload = .{
451 .kind = .reject_before_mutation,
452 .detail = "current or publication paths that exceed either admitted region reject before any region changes",
453 },
454 .risks = .{
455 .transitive = .{
456 .status = .open,
457 .detail = "path copies are allocation-free but no machine call-graph certificate excludes future transitive growth",
458 },
459 .foreign = .{
460 .status = .excluded,
461 .detail = "filesystem interpretation of copied path bytes remains a foreign operating-system effect",
462 },
463 },
464 .work = .{
465 .equation = "each replacement copies no more than the sum of its two admitted path-byte bounds",
466 },
467 .obligations = &.{
468 .{ .key = "sql_path_storage_capacity", .role = .capacity_model },
469 .{ .key = "sql_path_storage_rejection", .role = .initialization_failure },
470 .{ .key = "sql_path_storage_overload", .role = .overload },
471 .{ .key = "sql_path_storage_work", .role = .work_bound },
472 .{ .key = "sql_path_storage_transitive", .role = .transitive_risk },
473 .{ .key = "sql_path_storage_foreign", .role = .foreign_risk },
474 },
475 },
476 .bindings = .{
477 .owner = @This(),
478 .seal = .{
479 .family = alloc_phase.capacity.selector(@This().activate),
480 .premise = .{
481 .class = .checked_semantic_fact,
482 .authority = .checker,
483 },
484 },
485 .teardown = .{
486 .family = alloc_phase.capacity.selector(@This().deinit),
487 .premise = .{
488 .class = .checked_semantic_fact,
489 .authority = .checker,
490 },
491 },
492 },
493 };
494
495 phase: alloc_phase.capacity.Phase,
496 capacity: Capacity,
497 storage: Storage,
498 identity_value: u64,
499 owner_address: usize = 0,
500 loan_epoch: u64 = 0,
501 loan_live: bool = false,
502 loan_capacity: Capacity = undefined,
503
504 /// A borrow of one owner's path regions that an open database holds for as
505 /// long as it needs its path bytes to stay put. The loan copies a pair of
506 /// paths into the current regions, or into the publication base regions,
507 /// and returns slices of what it wrote, reporting `PathCapacityExceeded`
508 /// when a path is longer than the region the acquisition admitted. Because
509 /// each loan carries the owner it came from, that owner's identity number,
510 /// and the epoch of the acquisition, it reports `InvalidPathStorageLoan`
511 /// when the owner no longer recognizes it, which covers a released loan, a
512 /// later acquisition, and an owner that has moved.
513 pub const Loan = struct {
514 pub const LoanError = error{ InvalidPathStorageLoan, PathCapacityExceeded };
515
516 owner: *PathStorage,
517 identity_value: u64,
518 epoch: u64,
519
520 pub fn storeCurrent(self: *Loan, paths: Paths) LoanError!Paths {
521 try self.requireOwned();
522 if (!self.admitsCurrent(paths)) {
523 return error.PathCapacityExceeded;
524 }
525 const database = self.owner.databaseRegion()[0..paths.database.len];
526 const wal_path = self.owner.walRegion()[0..paths.wal.len];
527 @memcpy(database, paths.database);
528 @memcpy(wal_path, paths.wal);
529 return .{ .database = database, .wal = wal_path };
530 }
531
532 pub fn storePublication(self: *Loan, paths: Paths) LoanError!Paths {
533 try self.requireOwned();
534 if (!self.admitsPublication(paths)) {
535 return error.PathCapacityExceeded;
536 }
537 const database = self.owner.publicationDatabaseRegion()[0..paths.database.len];
538 const wal_path = self.owner.publicationWalRegion()[0..paths.wal.len];
539 @memcpy(database, paths.database);
540 @memcpy(wal_path, paths.wal);
541 return .{ .database = database, .wal = wal_path };
542 }
543
544 fn requireOwned(self: *const Loan) LoanError!void {
545 if (!self.owner.ownsLoan(self)) return error.InvalidPathStorageLoan;
546 }
547
548 fn admitsCurrent(self: *const Loan, paths: Paths) bool {
549 return paths.database.len <= self.owner.loan_capacity.database_bytes and
550 paths.wal.len <= self.owner.loan_capacity.wal_bytes;
551 }
552
553 fn admitsPublication(self: *const Loan, paths: Paths) bool {
554 return paths.database.len <= self.owner.loan_capacity.publication_database_bytes and
555 paths.wal.len <= self.owner.loan_capacity.publication_wal_bytes;
556 }
557 };
558
559 pub fn init(storage: Storage, limits: Limits) InitError!PathStorage {
560 const capacity = try Capacity.derive(limits);
561 const storage_bytes = capacity.storageBytes();
562 if (storage.len < storage_bytes) return error.StorageTooShort;
563 const identity_value = try path_storage_identities.take();
564 return .{
565 .phase = .initialization,
566 .capacity = capacity,
567 .storage = storage[0..storage_bytes],
568 .identity_value = identity_value,
569 };
570 }
571
572 pub fn activate(self: *PathStorage) void {
573 std.debug.assert(self.phase == .initialization);
574 std.debug.assert(self.storage.len == self.capacity.storageBytes());
575 std.debug.assert(self.owner_address == 0);
576 self.owner_address = @intFromPtr(self);
577 self.phase = .steady;
578 }
579
580 pub fn deinit(self: *PathStorage) Storage {
581 std.debug.assert(self.phase != .teardown);
582 std.debug.assert(self.storage.len == self.capacity.storageBytes());
583 std.debug.assert(!self.loan_live);
584 if (self.phase == .steady) self.requireOwned() catch unreachable;
585 self.phase = .teardown;
586 const storage = self.storage;
587 self.* = undefined;
588 return storage;
589 }
590
591 pub fn acquire(self: *PathStorage, limits: Limits) AcquireError!Loan {
592 const loan = try Capacity.derive(limits);
593 if (loan.database_bytes > self.capacity.database_bytes or
594 loan.wal_bytes > self.capacity.wal_bytes or
595 loan.publication_database_bytes > self.capacity.publication_database_bytes or
596 loan.publication_wal_bytes > self.capacity.publication_wal_bytes)
597 {
598 return error.PathCapacityExceeded;
599 }
600 if (self.loan_live) return error.PathStorageBusy;
601 if (self.loan_epoch == std.math.maxInt(u64)) {
602 return error.PathLoanGenerationExhausted;
603 }
604 if (self.phase == .initialization) {
605 self.activate();
606 } else {
607 try self.requireOwned();
608 }
609 self.loan_epoch += 1;
610 self.loan_live = true;
611 self.loan_capacity = loan;
612 return .{
613 .owner = self,
614 .identity_value = self.identity_value,
615 .epoch = self.loan_epoch,
616 };
617 }
618
619 pub fn release(self: *PathStorage, loan: *Loan) Loan.LoanError!void {
620 if (!self.ownsLoan(loan)) return error.InvalidPathStorageLoan;
621 self.loan_live = false;
622 self.loan_capacity = undefined;
623 loan.* = undefined;
624 }
625
626 pub fn ownsLoan(self: *const PathStorage, loan: *const Loan) bool {
627 self.requireOwned() catch return false;
628 return self.loan_live and
629 loan.owner == self and
630 loan.identity_value == self.identity_value and
631 loan.epoch == self.loan_epoch;
632 }
633
634 fn requireOwned(self: *const PathStorage) OwnerError!void {
635 if (self.phase != .steady or self.owner_address != @intFromPtr(self)) {
636 return error.InvalidPathStorageOwner;
637 }
638 }
639
640 fn databaseRegion(self: *PathStorage) []u8 {
641 return self.storage[0..self.capacity.database_bytes];
642 }
643
644 fn walRegion(self: *PathStorage) []u8 {
645 const start = self.capacity.database_bytes;
646 return self.storage[start .. start + self.capacity.wal_bytes];
647 }
648
649 fn publicationDatabaseRegion(self: *PathStorage) []u8 {
650 const start = self.capacity.database_bytes + self.capacity.wal_bytes;
651 return self.storage[start .. start + self.capacity.publication_database_bytes];
652 }
653
654 fn publicationWalRegion(self: *PathStorage) []u8 {
655 const start = self.capacity.database_bytes +
656 self.capacity.wal_bytes +
657 self.capacity.publication_database_bytes;
658 return self.storage[start .. start + self.capacity.publication_wal_bytes];
659 }
660
661 const PublicationPathBytes = struct {
662 database: usize,
663 wal: usize,
664 };
665
666 fn publicationPathBytes(base_paths: Paths) error{InvalidPublication}!PublicationPathBytes {
667 const lane = publication.Lane.init(.{
668 .database = base_paths.database,
669 .wal = base_paths.wal,
670 }, 0) catch return error.InvalidPublication;
671 const pair = lane.pair();
672 return .{ .database = pair.database.len, .wal = pair.wal.len };
673 }
674 };
675
676 comptime {
677 alloc_phase.capacity.requireProvisionedRejectingOwnerShape(PathStorage);
678 }
679
680 const RecoveryImage = struct {
681 page_id: u32,
682 frame: usize,
683 bytes: [page.size]u8,
684 };
685
686 /// Reuses a stored tree digest only when its page, count, and entire state match.
687 /// Zero entries disable reuse while preserving the same digest computation.
688 pub const DigestMemo = struct {
689 pub const Entry = struct {
690 identity_page: u32 = 0,
691 entries: u64 = 0,
692 state: lattice.State = undefined,
693 digest: [lattice.digest_size]u8 = undefined,
694 };
695
696 pub const Storage = []Entry;
697
698 pub const Capacity = struct {
699 entries: usize,
700 storage_bytes: usize,
701
702 pub fn derive(entries: usize) error{CapacityOverflow}!Capacity {
703 return .{
704 .entries = entries,
705 .storage_bytes = std.math.mul(usize, entries, @sizeOf(Entry)) catch
706 return error.CapacityOverflow,
707 };
708 }
709 };
710
711 slots: Storage,
712
713 pub fn init(storage: Storage, entries: usize) DigestMemo {
714 std.debug.assert(storage.len >= entries);
715 const slots = storage[0..entries];
716 for (slots) |*slot| slot.identity_page = 0;
717 return .{ .slots = slots };
718 }
719
720 pub fn digest(
721 self: *DigestMemo,
722 identity_page: u32,
723 state: *const lattice.State,
724 entries: u64,
725 ) [lattice.digest_size]u8 {
726 std.debug.assert(identity_page != 0);
727 if (self.slots.len == 0) return state.digest(entries);
728 const slot = &self.slots[@as(usize, identity_page) % self.slots.len];
729 if (slot.identity_page == identity_page and
730 slot.entries == entries and
731 slot.state.eql(state))
732 {
733 return slot.digest;
734 }
735 const hash = state.digest(entries);
736 slot.* = .{
737 .identity_page = identity_page,
738 .entries = entries,
739 .state = state.*,
740 .digest = hash,
741 };
742 return hash;
743 }
744 };
745
746 test "digest memo hit matches state digest" {
747 var storage: [2]DigestMemo.Entry = undefined;
748 var memo = DigestMemo.init(&storage, storage.len);
749 var state = lattice.State.empty;
750 state.lanes[17] = 42;
751 const expected = state.digest(3);
752 try std.testing.expectEqual(expected, memo.digest(5, &state, 3));
753 try std.testing.expectEqual(expected, memo.digest(5, &state, 3));
754 }
755
756 test "digest memo recomputes changed lane and entry count" {
757 var storage: [1]DigestMemo.Entry = undefined;
758 var memo = DigestMemo.init(&storage, storage.len);
759 var state = lattice.State.empty;
760 const original = memo.digest(1, &state, 1);
761 state.lanes[lattice.lane_count - 1] = 1;
762 const changed_lane = memo.digest(1, &state, 1);
763 try std.testing.expectEqual(state.digest(1), changed_lane);
764 try std.testing.expect(!std.mem.eql(u8, &original, &changed_lane));
765 const changed_count = memo.digest(1, &state, 2);
766 try std.testing.expectEqual(state.digest(2), changed_count);
767 try std.testing.expect(!std.mem.eql(u8, &changed_lane, &changed_count));
768 }
769
770 test "digest memo collision retains each identity digest" {
771 var storage: [1]DigestMemo.Entry = undefined;
772 var memo = DigestMemo.init(&storage, storage.len);
773 var first = lattice.State.empty;
774 var second = lattice.State.empty;
775 first.lanes[0] = 1;
776 second.lanes[0] = 2;
777 try std.testing.expectEqual(first.digest(1), memo.digest(1, &first, 1));
778 try std.testing.expectEqual(second.digest(1), memo.digest(2, &second, 1));
779 try std.testing.expectEqual(first.digest(1), memo.digest(1, &first, 1));
780 }
781
782 test "digest memo full capacity keeps replacing entries" {
783 var storage: [2]DigestMemo.Entry = undefined;
784 var memo = DigestMemo.init(&storage, storage.len);
785 var state = lattice.State.empty;
786 for (1..9) |page_number| {
787 state.lanes[page_number] = @intCast(page_number);
788 const page_id: u32 = @intCast(page_number);
789 try std.testing.expectEqual(
790 state.digest(page_number),
791 memo.digest(page_id, &state, page_number),
792 );
793 }
794 try std.testing.expectEqual(@as(usize, 2), memo.slots.len);
795 }
796
797 test "digest memo zero capacity computes directly" {
798 var memo = DigestMemo.init(&.{}, 0);
799 const state = lattice.State.empty;
800 try std.testing.expectEqual(state.digest(4), memo.digest(1, &state, 4));
801 }
802
803 /// The reusable storage an open database runs on, built once by a caller so
804 /// that the pager, the caches, and the paths for every database opened through
805 /// it come from storage laid out in advance. The workspace covers the pager,
806 /// path storage, read cache, digest memo, and transaction staging area. One set
807 /// of limits sizes all of them, naming the write-ahead log header, the log byte
808 /// ceiling, the path limits, read cache pages, and digest memo entries. The
809 /// workspace takes storage from the caller with `init` or from an allocator
810 /// with `allocate`, gives it back with the matching `deinit` or `deallocate`,
811 /// and lends each region to one holder at a time. A second request receives
812 /// `WorkspaceBusy`.
813 /// Because the path storage inside records the workspace's address, callers
814 /// keep the workspace at one address across every database opened from it, and
815 /// teardown refuses by assertion until every region has been returned.
816 pub const DatabaseWorkspace = struct {
817 pub const Limits = struct {
818 header: wal.Header,
819 max_wal_bytes: usize,
820 path_storage: PathStorage.Limits,
821 read_cache_pages: usize = default_read_cache_capacity,
822 /// Direct-mapped slots, keyed by identity page, each holding one
823 /// stored tree digest. Zero disables reuse.
824 digest_memo_entries: usize = default_digest_memo_entries,
825 };
826
827 pub const Storage = struct {
828 pager: pager.Pager.Workspace,
829 paths: PathStorage.Storage,
830 read_cache: ReadCache.Storage,
831 digest_memo: DigestMemo.Storage,
832 transaction_staging: TransactionStaging.Storage,
833 };
834
835 pub const Capacity = struct {
836 wal_frames: usize,
837 pager: pager.Pager.Workspace.Capacity,
838 paths: PathStorage.Capacity,
839 read_cache: ReadCache.Capacity,
840 digest_memo: DigestMemo.Capacity,
841 transaction_staging: TransactionStaging.Capacity,
842 requested_bytes: usize,
843
844 pub const DeriveError = pager.Pager.Workspace.Capacity.DeriveError ||
845 error{ InvalidWalLimit, CapacityOverflow };
846
847 pub fn derive(limits: Limits) DeriveError!Capacity {
848 const wal_frames = try walFrameCapacity(limits.max_wal_bytes);
849 const pager_capacity = try pager.Pager.Workspace.Capacity.derive(.{
850 .header = limits.header,
851 .wal_frames = wal_frames,
852 });
853 const transaction_staging = try TransactionStaging.Capacity.derive(.{
854 .frames = wal_frames,
855 });
856 const paths = try PathStorage.Capacity.derive(limits.path_storage);
857 const read_cache = try ReadCache.Capacity.derive(.{
858 .pages = limits.read_cache_pages,
859 });
860 const digest_memo = try DigestMemo.Capacity.derive(limits.digest_memo_entries);
861 var requested_bytes = pager_capacity.journal.storage_bytes;
862 requested_bytes = std.math.add(
863 usize,
864 requested_bytes,
865 pager_capacity.checkpoint.storage_bytes,
866 ) catch return error.CapacityOverflow;
867 requested_bytes = std.math.add(
868 usize,
869 requested_bytes,
870 pager_capacity.checkpoint_once.storage_bytes,
871 ) catch return error.CapacityOverflow;
872 requested_bytes = std.math.add(
873 usize,
874 requested_bytes,
875 pager_capacity.wal_index.storage_bytes,
876 ) catch return error.CapacityOverflow;
877 requested_bytes = std.math.add(
878 usize,
879 requested_bytes,
880 paths.storageBytes(),
881 ) catch return error.CapacityOverflow;
882 requested_bytes = std.math.add(
883 usize,
884 requested_bytes,
885 read_cache.storage_bytes,
886 ) catch return error.CapacityOverflow;
887 requested_bytes = std.math.add(
888 usize,
889 requested_bytes,
890 digest_memo.storage_bytes,
891 ) catch return error.CapacityOverflow;
892 requested_bytes = std.math.add(
893 usize,
894 requested_bytes,
895 transaction_staging.storage_bytes,
896 ) catch return error.CapacityOverflow;
897 return .{
898 .wal_frames = wal_frames,
899 .pager = pager_capacity,
900 .paths = paths,
901 .read_cache = read_cache,
902 .digest_memo = digest_memo,
903 .transaction_staging = transaction_staging,
904 .requested_bytes = requested_bytes,
905 };
906 }
907 };
908
909 pub const InitError = Capacity.DeriveError || PathStorage.IdentityError ||
910 error{StorageTooShort};
911 pub const AllocateError = Allocator.Error || Capacity.DeriveError ||
912 PathStorage.IdentityError;
913 pub const AcquireError = Error || error{ WorkspaceBusy, WorkspaceCapacityExceeded };
914
915 const State = enum {
916 free,
917 busy,
918 };
919
920 phase: alloc_phase.capacity.Phase,
921 limits: Limits,
922 capacity: Capacity,
923 storage: Storage,
924 path_storage: PathStorage,
925 state: State = .free,
926 loan: pager.Pager.Workspace.Capacity = undefined,
927 read_cache_live: bool = false,
928 read_cache_loan: ReadCache.Capacity = undefined,
929 digest_memo_live: bool = false,
930 transaction_staging_live: bool = false,
931 transaction_staging_loan: TransactionStaging.Capacity = undefined,
932
933 pub fn init(storage: Storage, limits: Limits) InitError!DatabaseWorkspace {
934 const capacity = try Capacity.derive(limits);
935 if (!fits(storage, capacity)) return error.StorageTooShort;
936 const path_storage = try PathStorage.init(
937 storage.paths[0..capacity.paths.storageBytes()],
938 limits.path_storage,
939 );
940 return .{
941 .phase = .initialization,
942 .limits = limits,
943 .capacity = capacity,
944 .storage = storage,
945 .path_storage = path_storage,
946 };
947 }
948
949 pub fn allocate(allocator: Allocator, limits: Limits) AllocateError!DatabaseWorkspace {
950 const capacity = try Capacity.derive(limits);
951 var pager_storage = try pager.Pager.Workspace.allocate(allocator, .{
952 .header = limits.header,
953 .wal_frames = capacity.wal_frames,
954 });
955 errdefer pager_storage.deallocate(allocator);
956 const paths = if (capacity.paths.storageBytes() == 0)
957 @as(PathStorage.Storage, &.{})
958 else
959 try allocator.alloc(u8, capacity.paths.storageBytes());
960 errdefer if (paths.len != 0) allocator.free(paths);
961 const read_cache = if (capacity.read_cache.storage_bytes == 0)
962 @as(ReadCache.Storage, &.{})
963 else
964 try allocator.alignedAlloc(
965 u8,
966 .fromByteUnits(ReadCache.storage_alignment),
967 capacity.read_cache.storage_bytes,
968 );
969 errdefer if (read_cache.len != 0) allocator.free(read_cache);
970 const digest_memo = if (capacity.digest_memo.entries == 0)
971 @as(DigestMemo.Storage, &.{})
972 else
973 try allocator.alloc(DigestMemo.Entry, capacity.digest_memo.entries);
974 errdefer if (digest_memo.len != 0) allocator.free(digest_memo);
975 const transaction_staging = if (capacity.transaction_staging.storage_bytes == 0)
976 @as(TransactionStaging.Storage, &.{})
977 else
978 try allocator.alignedAlloc(
979 u8,
980 .fromByteUnits(TransactionStaging.storage_alignment),
981 capacity.transaction_staging.storage_bytes,
982 );
983 errdefer if (transaction_staging.len != 0) allocator.free(transaction_staging);
984 var workspace = init(.{
985 .pager = pager_storage,
986 .paths = paths,
987 .read_cache = read_cache,
988 .digest_memo = digest_memo,
989 .transaction_staging = transaction_staging,
990 }, limits) catch |err| switch (err) {
991 error.StorageTooShort => unreachable,
992 else => |other| return other,
993 };
994 workspace.activate();
995 return workspace;
996 }
997
998 pub fn activate(self: *DatabaseWorkspace) void {
999 std.debug.assert(self.phase == .initialization);
1000 self.assertValid();
1001 self.phase = .steady;
1002 }
1003
1004 pub fn acquire(
1005 self: *DatabaseWorkspace,
1006 allocator: Allocator,
1007 options: pager.InitOptions,
1008 ) AcquireError!pager.Pager {
1009 self.assertSteady();
1010 if (self.state == .busy) return error.WorkspaceBusy;
1011 const loan = try pager.Pager.Workspace.Capacity.derive(options);
1012 if (options.wal_frames > self.capacity.wal_frames or
1013 !fitsPager(self.storage.pager, loan))
1014 {
1015 return error.WorkspaceCapacityExceeded;
1016 }
1017 var storage = pager.Pager.Workspace.init(
1018 self.storage.pager.journal[0..loan.journal.storage_bytes],
1019 self.storage.pager.checkpoint[0..loan.checkpoint.storage_bytes],
1020 self.storage.pager.checkpoint_once[0..loan.checkpoint_once.storage_bytes],
1021 self.storage.pager.wal_index[0..loan.wal_index.storage_bytes],
1022 );
1023 const owned = pager.Pager.init(allocator, &storage, options) catch |err| switch (err) {
1024 error.StorageTooShort => unreachable,
1025 else => return err,
1026 };
1027 self.state = .busy;
1028 self.loan = loan;
1029 return owned;
1030 }
1031
1032 pub fn release(self: *DatabaseWorkspace, owned: *pager.Pager) void {
1033 self.assertSteady();
1034 std.debug.assert(self.state == .busy);
1035 const returned = owned.deinit();
1036 assertReturned(self.storage.pager.journal, returned.journal, self.loan.journal.storage_bytes);
1037 assertReturned(self.storage.pager.checkpoint, returned.checkpoint, self.loan.checkpoint.storage_bytes);
1038 assertReturned(
1039 self.storage.pager.checkpoint_once,
1040 returned.checkpoint_once,
1041 self.loan.checkpoint_once.storage_bytes,
1042 );
1043 assertReturned(self.storage.pager.wal_index, returned.wal_index, self.loan.wal_index.storage_bytes);
1044 self.state = .free;
1045 self.loan = undefined;
1046 }
1047
1048 pub fn acquirePathStorage(
1049 self: *DatabaseWorkspace,
1050 limits: PathStorage.Limits,
1051 ) AcquireError!PathStorage.Loan {
1052 self.assertSteady();
1053 return self.path_storage.acquire(limits) catch |err| switch (err) {
1054 error.PathStorageBusy => error.WorkspaceBusy,
1055 error.PathCapacityExceeded => error.WorkspaceCapacityExceeded,
1056 else => |other| other,
1057 };
1058 }
1059
1060 pub fn releasePathStorage(
1061 self: *DatabaseWorkspace,
1062 owned: *PathStorage.Loan,
1063 ) PathStorage.Loan.LoanError!void {
1064 self.assertSteady();
1065 try self.path_storage.release(owned);
1066 }
1067
1068 pub fn acquireReadCache(
1069 self: *DatabaseWorkspace,
1070 limits: ReadCache.Limits,
1071 ) AcquireError!ReadCache {
1072 self.assertSteady();
1073 if (self.read_cache_live) return error.WorkspaceBusy;
1074 const loan = try ReadCache.Capacity.derive(limits);
1075 if (loan.storage_bytes > self.storage.read_cache.len) {
1076 return error.WorkspaceCapacityExceeded;
1077 }
1078 const owned = ReadCache.init(
1079 self.storage.read_cache[0..loan.storage_bytes],
1080 limits,
1081 ) catch |err| switch (err) {
1082 error.StorageTooShort => unreachable,
1083 else => return err,
1084 };
1085 self.read_cache_live = true;
1086 self.read_cache_loan = loan;
1087 return owned;
1088 }
1089
1090 pub fn releaseReadCache(
1091 self: *DatabaseWorkspace,
1092 owned: *ReadCache,
1093 ) void {
1094 self.assertSteady();
1095 std.debug.assert(self.read_cache_live);
1096 const returned = owned.deinit();
1097 assertReturned(
1098 self.storage.read_cache,
1099 returned,
1100 self.read_cache_loan.storage_bytes,
1101 );
1102 self.read_cache_live = false;
1103 self.read_cache_loan = undefined;
1104 }
1105
1106 pub fn acquireDigestMemo(self: *DatabaseWorkspace) AcquireError!DigestMemo {
1107 self.assertSteady();
1108 if (self.digest_memo_live) return error.WorkspaceBusy;
1109 self.digest_memo_live = true;
1110 return DigestMemo.init(self.storage.digest_memo, self.capacity.digest_memo.entries);
1111 }
1112
1113 pub fn releaseDigestMemo(self: *DatabaseWorkspace, owned: *DigestMemo) void {
1114 self.assertSteady();
1115 std.debug.assert(self.digest_memo_live);
1116 std.debug.assert(owned.slots.ptr == self.storage.digest_memo.ptr);
1117 std.debug.assert(owned.slots.len == self.capacity.digest_memo.entries);
1118 owned.* = undefined;
1119 self.digest_memo_live = false;
1120 }
1121
1122 pub fn acquireTransactionStaging(
1123 self: *DatabaseWorkspace,
1124 limits: TransactionStaging.Limits,
1125 ) AcquireError!TransactionStaging {
1126 self.assertSteady();
1127 if (self.transaction_staging_live) return error.WorkspaceBusy;
1128 const loan = try TransactionStaging.Capacity.derive(limits);
1129 if (loan.storage_bytes > self.storage.transaction_staging.len) {
1130 return error.WorkspaceCapacityExceeded;
1131 }
1132 const owned = TransactionStaging.init(
1133 self.storage.transaction_staging[0..loan.storage_bytes],
1134 limits,
1135 ) catch |err| switch (err) {
1136 error.StorageTooShort => unreachable,
1137 else => return err,
1138 };
1139 self.transaction_staging_live = true;
1140 self.transaction_staging_loan = loan;
1141 return owned;
1142 }
1143
1144 pub fn releaseTransactionStaging(
1145 self: *DatabaseWorkspace,
1146 owned: *TransactionStaging,
1147 ) void {
1148 self.assertSteady();
1149 std.debug.assert(self.transaction_staging_live);
1150 const returned = owned.deinit();
1151 assertReturned(
1152 self.storage.transaction_staging,
1153 returned,
1154 self.transaction_staging_loan.storage_bytes,
1155 );
1156 self.transaction_staging_live = false;
1157 self.transaction_staging_loan = undefined;
1158 }
1159
1160 pub fn deinit(self: *DatabaseWorkspace) Storage {
1161 std.debug.assert(self.phase != .teardown);
1162 self.assertValid();
1163 std.debug.assert(self.state == .free);
1164 std.debug.assert(!self.path_storage.loan_live);
1165 std.debug.assert(!self.read_cache_live);
1166 std.debug.assert(!self.digest_memo_live);
1167 std.debug.assert(!self.transaction_staging_live);
1168 const returned_paths = self.path_storage.deinit();
1169 assertReturned(
1170 self.storage.paths,
1171 returned_paths,
1172 self.capacity.paths.storageBytes(),
1173 );
1174 self.phase = .teardown;
1175 const storage = self.storage;
1176 self.* = undefined;
1177 return storage;
1178 }
1179
1180 pub fn deallocate(self: *DatabaseWorkspace, allocator: Allocator) void {
1181 var storage = self.deinit();
1182 if (storage.transaction_staging.len != 0) {
1183 allocator.free(storage.transaction_staging);
1184 }
1185 if (storage.read_cache.len != 0) allocator.free(storage.read_cache);
1186 if (storage.digest_memo.len != 0) allocator.free(storage.digest_memo);
1187 if (storage.paths.len != 0) allocator.free(storage.paths);
1188 storage.pager.deallocate(allocator);
1189 }
1190
1191 pub fn retainedBytes(self: *const DatabaseWorkspace) usize {
1192 self.assertValid();
1193 return self.capacity.requested_bytes;
1194 }
1195
1196 fn assertSteady(self: *const DatabaseWorkspace) void {
1197 std.debug.assert(self.phase == .steady);
1198 self.assertValid();
1199 }
1200
1201 fn assertValid(self: *const DatabaseWorkspace) void {
1202 std.debug.assert(fits(self.storage, self.capacity));
1203 }
1204
1205 fn fits(
1206 storage: Storage,
1207 capacity: Capacity,
1208 ) bool {
1209 return fitsPager(storage.pager, capacity.pager) and
1210 storage.paths.len >= capacity.paths.storageBytes() and
1211 storage.read_cache.len >= capacity.read_cache.storage_bytes and
1212 storage.digest_memo.len >= capacity.digest_memo.entries and
1213 storage.transaction_staging.len >= capacity.transaction_staging.storage_bytes;
1214 }
1215
1216 fn fitsPager(
1217 storage: pager.Pager.Workspace,
1218 capacity: pager.Pager.Workspace.Capacity,
1219 ) bool {
1220 return storage.journal.len >= capacity.journal.storage_bytes and
1221 storage.checkpoint.len >= capacity.checkpoint.storage_bytes and
1222 storage.checkpoint_once.len >= capacity.checkpoint_once.storage_bytes and
1223 storage.wal_index.len >= capacity.wal_index.storage_bytes;
1224 }
1225
1226 fn assertReturned(full: []u8, returned: []u8, loan_bytes: usize) void {
1227 std.debug.assert(returned.ptr == full.ptr);
1228 std.debug.assert(returned.len == loan_bytes);
1229 std.debug.assert(returned.len <= full.len);
1230 }
1231 };
1232
1233 const RecoveryState = struct {
1234 workspace: *DatabaseWorkspace,
1235 pager: pager.Pager,
1236 rewrite_base: bool,
1237 rewrite_wal: bool,
1238 base_loaded: bool,
1239
1240 fn deinit(self: *RecoveryState) void {
1241 self.workspace.release(&self.pager);
1242 self.* = undefined;
1243 }
1244 };
1245
1246 const WalRecovery = struct {
1247 committed_frames: usize = 0,
1248 rewrite_base: bool = false,
1249 rewrite_wal: bool = false,
1250 base_loaded: bool = false,
1251 };
1252
1253 const ReadCacheKey = struct {
1254 generation: u64,
1255 page_id: u32,
1256 /// The check mark of the cached image, which `put` clears.
1257 checked: bool = false,
1258 };
1259
1260 const ReadCacheImage = [page.size]u8;
1261
1262 pub const ReadCache = struct {
1263 pub const storage_alignment: usize = @alignOf(ReadCacheKey);
1264 pub const Storage = []align(storage_alignment) u8;
1265
1266 pub const Limits = struct {
1267 pages: usize,
1268 };
1269
1270 pub const Capacity = struct {
1271 pages: usize,
1272 storage_bytes: usize,
1273
1274 pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity {
1275 const key_bytes = std.math.mul(usize, limits.pages, @sizeOf(ReadCacheKey)) catch {
1276 return error.CapacityOverflow;
1277 };
1278 const image_bytes = std.math.mul(usize, limits.pages, @sizeOf(ReadCacheImage)) catch {
1279 return error.CapacityOverflow;
1280 };
1281 const storage_bytes = std.math.add(usize, key_bytes, image_bytes) catch {
1282 return error.CapacityOverflow;
1283 };
1284 return .{
1285 .pages = limits.pages,
1286 .storage_bytes = storage_bytes,
1287 };
1288 }
1289 };
1290
1291 pub const InitError = error{ CapacityOverflow, StorageTooShort };
1292 pub const Exhaustion = error{CacheDisabled};
1293 pub const work_limits: alloc_phase.capacity.WorkLimits = .{
1294 .transition_steps_max = std.math.maxInt(usize),
1295 .cleanup_steps_per_call_max = 0,
1296 .cleanup_calls_at_capacity_max = 0,
1297 };
1298
1299 pub const claim: alloc_phase.capacity.Declaration = .{
1300 .source = .{
1301 .id = "sql.file_read_cache",
1302 .kind = .phase_static,
1303 .limit_source = .caller,
1304 .storage = .{
1305 .covered = &.{
1306 .{
1307 .id = "configured_direct_mapped_base_page_cache_slots_for_94cfd2eab286",
1308 .lifetime = .steady,
1309 .detail = "configured direct-mapped base-page cache slots for lazy and durable-checkpoint reads",
1310 },
1311 },
1312 .excluded = &.{
1313 "operating-system page cache, database and WAL file contents, and file handles",
1314 "pager-owned WAL images, indexes, prepared checkpoint descriptors, and reader-bearing in-memory base history",
1315 "caller-owned copyPage destination images and trace instrumentation",
1316 },
1317 },
1318 .capacity = .{
1319 .inputs = &.{
1320 alloc_phase.capacity.bindInput(Limits, "pages", "pages"),
1321 },
1322 .type_selectors = &.{
1323 alloc_phase.capacity.bindType(ReadCacheKey, "readcachekey"),
1324 alloc_phase.capacity.bindType(ReadCacheImage, "readcacheimage"),
1325 },
1326 .nodes = &.{
1327 .{ .input = 0 },
1328 .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },
1329 .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 1 } } },
1330 .{ .add = .{ .left = 1, .right = 2 } },
1331 },
1332 .assertions = &.{.{
1333 .scope = .closure_total,
1334 .measure = .retained,
1335 .relation = .exact,
1336 .expression = 3,
1337 }},
1338 },
1339 .overload = .{
1340 .kind = .reject_before_mutation,
1341 .detail = "short caller storage rejects initialization and zero cache capacity returns CacheDisabled before mutation; admitted positive capacity replaces exactly page_id modulo slots",
1342 },
1343 .risks = .{
1344 .transitive = .{
1345 .status = .open,
1346 .detail = "cache methods are allocation-free value copies, but no machine call-graph certificate excludes future transitive growth",
1347 },
1348 .foreign = .{
1349 .status = .excluded,
1350 .detail = "file reads durable checkpoint writes and the operating-system page cache are outside the owner-local heap-storage claim",
1351 },
1352 },
1353 .work = .{
1354 .equation = "initialization and clear visit only the contiguous key bound, occupancy counting visits the admitted key bound, and get and put take constant steps",
1355 },
1356 .obligations = &.{
1357 .{ .key = "sql_read_cache_capacity", .role = .capacity_model },
1358 .{ .key = "sql_read_cache_storage_rejection", .role = .initialization_failure },
1359 .{ .key = "sql_read_cache_sealed_overload", .role = .overload },
1360 .{ .key = "sql_read_cache_work_bound", .role = .work_bound },
1361 .{ .key = "sql_read_cache_sealed_transitive_risk", .role = .transitive_risk },
1362 .{ .key = "sql_read_cache_semantics_overload", .role = .overload },
1363 .{ .key = "sql_read_cache_semantics_foreign_risk", .role = .foreign_risk },
1364 .{ .key = "sql_read_cache_checkpoint_semantics", .role = .foreign_risk },
1365 },
1366 },
1367 .bindings = .{
1368 .owner = @This(),
1369 .seal = .{
1370 .family = alloc_phase.capacity.selector(@This().activate),
1371 .premise = .{
1372 .class = .checked_semantic_fact,
1373 .authority = .checker,
1374 },
1375 },
1376 .teardown = .{
1377 .family = alloc_phase.capacity.selector(@This().deinit),
1378 .premise = .{
1379 .class = .checked_semantic_fact,
1380 .authority = .checker,
1381 },
1382 },
1383 },
1384 };
1385
1386 phase: alloc_phase.capacity.Phase,
1387 capacity: Capacity,
1388 storage: Storage,
1389 keys: []ReadCacheKey,
1390
1391 pub fn init(storage: Storage, limits: Limits) InitError!ReadCache {
1392 const capacity = try Capacity.derive(limits);
1393 if (storage.len < capacity.storage_bytes) return error.StorageTooShort;
1394 const borrowed = storage[0..capacity.storage_bytes];
1395 const keys = keysFromStorage(borrowed, capacity);
1396 for (keys) |*key| key.* = .{ .generation = 0, .page_id = 0 };
1397 return .{
1398 .phase = .initialization,
1399 .capacity = capacity,
1400 .storage = borrowed,
1401 .keys = keys,
1402 };
1403 }
1404
1405 pub fn activate(self: *ReadCache) void {
1406 std.debug.assert(self.phase == .initialization);
1407 std.debug.assert(self.keys.len == self.capacity.pages);
1408 std.debug.assert(imagesFromStorage(self.storage, self.capacity).len == self.capacity.pages);
1409 self.phase = .steady;
1410 }
1411
1412 pub fn deinit(self: *ReadCache) Storage {
1413 std.debug.assert(self.phase != .teardown);
1414 std.debug.assert(self.storage.len == self.capacity.storage_bytes);
1415 std.debug.assert(self.keys.len == self.capacity.pages);
1416 std.debug.assert(self.keys.ptr == keysFromStorage(self.storage, self.capacity).ptr);
1417 self.phase = .teardown;
1418 const storage = self.storage;
1419 self.* = undefined;
1420 return storage;
1421 }
1422
1423 fn clearRetainingCapacity(self: *ReadCache) void {
1424 std.debug.assert(self.phase == .steady);
1425 for (self.keys) |*key| key.* = .{ .generation = 0, .page_id = 0 };
1426 }
1427
1428 fn get(self: *ReadCache, key: ReadCacheKey, image: *[page.size]u8) bool {
1429 std.debug.assert(self.phase == .steady);
1430 std.debug.assert(key.page_id != 0);
1431 if (self.keys.len == 0) return false;
1432 const slot = cacheSlot(key, self.keys.len);
1433 const cached = self.keys[slot];
1434 if (cached.generation != key.generation or cached.page_id != key.page_id) return false;
1435 image.* = imagesFromStorage(self.storage, self.capacity)[slot];
1436 return true;
1437 }
1438
1439 pub fn put(self: *ReadCache, key: ReadCacheKey, image: *const [page.size]u8) Exhaustion!void {
1440 std.debug.assert(self.phase == .steady);
1441 std.debug.assert(key.page_id != 0);
1442 if (self.keys.len == 0) return error.CacheDisabled;
1443 const slot = cacheSlot(key, self.keys.len);
1444 imagesFromStorage(self.storage, self.capacity)[slot] = image.*;
1445 self.keys[slot] = .{ .generation = key.generation, .page_id = key.page_id };
1446 }
1447
1448 /// Returns the check mark of the cached image for `key`, or null when the
1449 /// cache does not hold that image.
1450 fn checkMark(self: *ReadCache, key: ReadCacheKey) ?*bool {
1451 std.debug.assert(self.phase == .steady);
1452 if (self.keys.len == 0) return null;
1453 const cached = &self.keys[cacheSlot(key, self.keys.len)];
1454 if (cached.generation != key.generation or cached.page_id != key.page_id) return null;
1455 return &cached.checked;
1456 }
1457
1458 fn count(self: *const ReadCache) usize {
1459 std.debug.assert(self.phase == .steady);
1460 var total: usize = 0;
1461 for (self.keys) |key| {
1462 if (key.page_id != 0) total += 1;
1463 }
1464 return total;
1465 }
1466
1467 fn keysFromStorage(storage: Storage, capacity: Capacity) []ReadCacheKey {
1468 return std.mem.bytesAsSlice(ReadCacheKey, storage[0..keyBytes(capacity)]);
1469 }
1470
1471 fn imagesFromStorage(storage: Storage, capacity: Capacity) []ReadCacheImage {
1472 return std.mem.bytesAsSlice(
1473 ReadCacheImage,
1474 storage[keyBytes(capacity)..capacity.storage_bytes],
1475 );
1476 }
1477
1478 fn keyBytes(capacity: Capacity) usize {
1479 return std.math.mul(usize, capacity.pages, @sizeOf(ReadCacheKey)) catch unreachable;
1480 }
1481 };
1482
1483 comptime {
1484 alloc_phase.capacity.requireProvisionedRejectingOwnerShape(ReadCache);
1485 }
1486
1487 fn cacheSlot(key: ReadCacheKey, capacity: usize) usize {
1488 return @as(usize, @intCast(key.page_id)) % capacity;
1489 }
1490
1491 pub const CommitDurability = enum {
1492 buffered,
1493 synced,
1494 };
1495
1496 pub const CommitOptions = struct {
1497 durability: CommitDurability = .synced,
1498 validate_indexes: bool = false,
1499 };
1500
1501 pub const Commit = struct {
1502 view: pager.View,
1503 frames: usize,
1504 pages: usize,
1505 synced: bool,
1506 };
1507
1508 pub const CheckpointOptions = struct {
1509 restart_header: ?wal.Header = null,
1510 control: wal.Control = .{},
1511 };
1512
1513 pub const Savepoint = struct {
1514 database: *Database,
1515 position: pager.Pager.Position,
1516 epoch: pager.Pager.RestoreEpoch,
1517 wal_written: usize,
1518 state_serial: u64,
1519 write_serial: u64,
1520
1521 pub fn same(self: Savepoint, other: Savepoint) bool {
1522 return self.database == other.database and
1523 std.meta.eql(self.position, other.position) and
1524 std.meta.eql(self.epoch, other.epoch) and
1525 self.wal_written == other.wal_written and
1526 self.state_serial == other.state_serial and
1527 self.write_serial == other.write_serial;
1528 }
1529 };
1530
1531 pub const WalIo = struct {
1532 writes: usize = 0,
1533 /// Truncations of the log file. An append ends the file where its
1534 /// write stops, so only a restore or a failed append truncates it.
1535 resizes: usize = 0,
1536 syncs: usize = 0,
1537 };
1538
1539 const PublicationState = struct {
1540 base_paths: Paths,
1541 selected: ?publication.Token,
1542 candidate: publication.Token,
1543 candidate_lane: u1 = 0,
1544 phase: enum { selected, candidate, sealed },
1545
1546 fn init(
1547 path_storage: *PathStorage.Loan,
1548 options: ?PublicationOptions,
1549 ) Error!?PublicationState {
1550 const value = options orelse return null;
1551 if (value.candidate == 0) return error.InvalidPublication;
1552 if (value.selected) |selected| {
1553 if (selected == 0 or selected == value.candidate) {
1554 return error.InvalidPublication;
1555 }
1556 }
1557 const base_paths = path_storage.storePublication(value.base_paths) catch unreachable;
1558 return .{
1559 .base_paths = base_paths,
1560 .selected = value.selected,
1561 .candidate = value.candidate,
1562 .phase = if (value.selected == null) .candidate else .selected,
1563 };
1564 }
1565
1566 fn deinit(self: *PublicationState) void {
1567 self.* = undefined;
1568 }
1569
1570 fn basePair(self: *const PublicationState) publication.Pair {
1571 return .{
1572 .database = self.base_paths.database,
1573 .wal = self.base_paths.wal,
1574 };
1575 }
1576 };
1577
1578 const ReadLeaseFile = union(enum) {
1579 owned: std.Io.File,
1580 borrowed: *const std.Io.File,
1581
1582 fn file(self: *const ReadLeaseFile) *const std.Io.File {
1583 return switch (self.*) {
1584 .owned => |*value| value,
1585 .borrowed => |value| value,
1586 };
1587 }
1588
1589 fn deinit(self: *ReadLeaseFile, io: std.Io) void {
1590 switch (self.*) {
1591 .owned => |value| value.close(io),
1592 .borrowed => {},
1593 }
1594 self.* = undefined;
1595 }
1596 };
1597
1598 const ReadLeaseSlot = struct {
1599 base_file: ?ReadLeaseFile = null,
1600 view: pager.View = undefined,
1601 base_epoch: u64 = 0,
1602 serial: u64 = 0,
1603 references: u16 = 0,
1604 };
1605
1606 const ReadLeaseRegistry = struct {
1607 pub const Limits = struct {
1608 leases: u8,
1609 };
1610
1611 pub const Exhaustion = error{ReadLeaseCapacityExceeded};
1612 pub const InitError = error{InvalidReadLeaseLimit};
1613
1614 slots: [read_lease_capacity_max]ReadLeaseSlot = @splat(.{}),
1615 limit: u8,
1616 active: u8 = 0,
1617
1618 pub fn init(limits: Limits) InitError!ReadLeaseRegistry {
1619 if (limits.leases > read_lease_capacity_max) {
1620 return error.InvalidReadLeaseLimit;
1621 }
1622 return .{ .limit = limits.leases };
1623 }
1624
1625 fn available(self: *const ReadLeaseRegistry) Exhaustion!u8 {
1626 if (self.active >= self.limit) return error.ReadLeaseCapacityExceeded;
1627 for (self.slots[0..self.limit], 0..) |slot, index| {
1628 if (slot.references == 0) return @intCast(index);
1629 }
1630 unreachable;
1631 }
1632
1633 fn install(
1634 self: *ReadLeaseRegistry,
1635 index: u8,
1636 base_file: ReadLeaseFile,
1637 view: pager.View,
1638 base_epoch: u64,
1639 ) Error!u64 {
1640 const slot = &self.slots[index];
1641 std.debug.assert(slot.references == 0);
1642 std.debug.assert(slot.base_file == null);
1643 const serial = std.math.add(u64, slot.serial, 1) catch
1644 return error.GenerationOverflow;
1645 if (serial == 0) return error.GenerationOverflow;
1646 slot.* = .{
1647 .base_file = base_file,
1648 .view = view,
1649 .base_epoch = base_epoch,
1650 .serial = serial,
1651 .references = 1,
1652 };
1653 self.active += 1;
1654 return serial;
1655 }
1656
1657 fn retain(self: *ReadLeaseRegistry, index: u8, serial: u64) Error!void {
1658 const slot = try self.live(index, serial);
1659 slot.references = std.math.add(u16, slot.references, 1) catch
1660 return error.ReadLeaseCapacityExceeded;
1661 }
1662
1663 fn release(
1664 self: *ReadLeaseRegistry,
1665 io: std.Io,
1666 index: u8,
1667 serial: u64,
1668 ) void {
1669 const slot = self.live(index, serial) catch unreachable;
1670 std.debug.assert(slot.references > 0);
1671 slot.references -= 1;
1672 if (slot.references != 0) return;
1673 if (slot.base_file) |*base_file| {
1674 base_file.deinit(io);
1675 } else unreachable;
1676 slot.base_file = null;
1677 slot.base_epoch = 0;
1678 self.active -= 1;
1679 }
1680
1681 fn live(
1682 self: *ReadLeaseRegistry,
1683 index: u8,
1684 serial: u64,
1685 ) error{InvalidReadLease}!*ReadLeaseSlot {
1686 if (index >= self.limit) return error.InvalidReadLease;
1687 const slot = &self.slots[index];
1688 if (slot.references == 0 or slot.serial != serial) {
1689 return error.InvalidReadLease;
1690 }
1691 return slot;
1692 }
1693
1694 fn isLive(
1695 self: *const ReadLeaseRegistry,
1696 index: u8,
1697 serial: u64,
1698 ) bool {
1699 if (index >= self.limit) return false;
1700 const slot = &self.slots[index];
1701 return slot.references != 0 and slot.serial == serial;
1702 }
1703
1704 fn oldest(self: *const ReadLeaseRegistry) ?pager.View {
1705 var result: ?pager.View = null;
1706 for (self.slots[0..self.limit]) |slot| {
1707 if (slot.references == 0) continue;
1708 if (result) |*view| {
1709 view.base_generation = @min(view.base_generation, slot.view.base_generation);
1710 view.end_mark = @min(view.end_mark, slot.view.end_mark);
1711 } else {
1712 result = slot.view;
1713 }
1714 }
1715 return result;
1716 }
1717
1718 fn hasEpoch(self: *const ReadLeaseRegistry, base_epoch: u64) bool {
1719 for (self.slots[0..self.limit]) |slot| {
1720 if (slot.references != 0 and slot.base_epoch == base_epoch) return true;
1721 }
1722 return false;
1723 }
1724
1725 fn deinit(self: *ReadLeaseRegistry) void {
1726 std.debug.assert(self.active == 0);
1727 for (self.slots) |slot| {
1728 std.debug.assert(slot.references == 0);
1729 std.debug.assert(slot.base_file == null);
1730 }
1731 self.* = undefined;
1732 }
1733 };
1734
1735 /// An open database over its two files, held for as long as a caller uses the
1736 /// database to read and write through the scopes it hands out. The database
1737 /// owns its own directory handle, the base file, the write-ahead log file, and
1738 /// the copies of both paths it took at open time, while holding the pager, the
1739 /// read cache, digest memo, transaction staging area, and registry of live read
1740 /// leases, all borrowed from the workspace. The database allows one write
1741 /// transaction at a time, tracks whether one is open, and carries a recovery
1742 /// flag that a failed write sets, after which every operation refuses with
1743 /// `RecoveryRequired`. Releasing the database with `deinit` is correct only
1744 /// once every read lease and the write transaction have ended, which `deinit`
1745 /// asserts.
1746 pub const Database = struct {
1747 pub const Workspace = DatabaseWorkspace;
1748
1749 allocator: Allocator,
1750 workspace: *Workspace,
1751 io: std.Io,
1752 dir: std.Io.Dir,
1753 path_storage: PathStorage.Loan,
1754 paths: Paths,
1755 base_file: std.Io.File,
1756 wal_file: std.Io.File,
1757 pager: pager.Pager,
1758 /// The length of the log file. Every write, truncation and
1759 /// replacement of the file sets it, so an append that starts here
1760 /// ends the file where its write stops.
1761 wal_written: usize,
1762 wal_synced: bool = true,
1763 max_wal_bytes: usize,
1764 wal_io: WalIo = .{},
1765 base_loaded: bool,
1766 read_cache: ReadCache,
1767 digest_memo: DigestMemo,
1768 read_leases: ReadLeaseRegistry,
1769 base_epoch: u64 = 1,
1770 tree_roots: tree.RootCache = .{},
1771 transaction_staging: TransactionStaging,
1772 write_capacity: pager.Capacity = .{},
1773 write_reserved: bool = false,
1774 write_transaction_open: bool = false,
1775 coordinator_active: bool = false,
1776 recovery_required: bool = false,
1777 writable: bool = true,
1778 state_serial: u64 = 0,
1779 write_serial: u64 = 0,
1780 publication_state: ?PublicationState = null,
1781 testing_workspace_owned: if (builtin.is_test) bool else void = if (builtin.is_test) false else {},
1782
1783 pub fn openForTesting(
1784 allocator: Allocator,
1785 dir: std.Io.Dir,
1786 options: OpenOptions,
1787 ) Error!Database {
1788 if (!builtin.is_test) @compileError("openForTesting is available only in tests");
1789 const workspace = try allocator.create(Workspace);
1790 errdefer allocator.destroy(workspace);
1791 const path_storage_limits = try PathStorage.Limits.forOpen(options);
1792 workspace.* = try Workspace.allocate(allocator, .{
1793 .header = options.header,
1794 .max_wal_bytes = options.wal_capacity_bytes orelse options.max_wal_bytes,
1795 .path_storage = path_storage_limits,
1796 .read_cache_pages = options.read_cache_capacity,
1797 });
1798 errdefer workspace.deallocate(allocator);
1799 var database = try open(allocator, workspace, dir, options);
1800 database.testing_workspace_owned = true;
1801 return database;
1802 }
1803
1804 pub fn ownWorkspaceForTesting(self: *Database) void {
1805 if (!builtin.is_test) @compileError("ownWorkspaceForTesting is available only in tests");
1806 std.debug.assert(!self.testing_workspace_owned);
1807 self.testing_workspace_owned = true;
1808 }
1809
1810 /// Opens the configured database so that a caller gets a usable database
1811 /// whether the files are new, intact, or left mid-write by an earlier run,
1812 /// recovering whatever the last run left behind by replaying the
1813 /// write-ahead log and rewriting the base file or the log when recovery
1814 /// calls for it. The call borrows the path storage, the read cache, and the
1815 /// transaction staging area from the workspace, and takes its own handle on
1816 /// the directory and its own copies of both paths so that the caller's
1817 /// handle and path strings can go. A failure at any step gives every
1818 /// borrowed region and every opened handle back before it returns, so the
1819 /// workspace is reusable, while a failure to read or write the files after
1820 /// the database is open poisons it with `RecoveryRequired`, and the caller
1821 /// reopens.
1822 pub fn open(
1823 allocator: Allocator,
1824 workspace: *Workspace,
1825 dir: std.Io.Dir,
1826 options: OpenOptions,
1827 ) Error!Database {
1828 const phase = trace.scope("file.database.open");
1829 defer phase.end();
1830 const wal_capacity_bytes = options.wal_capacity_bytes orelse options.max_wal_bytes;
1831 if (wal_capacity_bytes > options.max_wal_bytes) return error.InvalidWalLimit;
1832 const path_storage_limits = try PathStorage.Limits.forOpen(options);
1833 var path_storage = try workspace.acquirePathStorage(path_storage_limits);
1834 errdefer workspace.releasePathStorage(&path_storage) catch unreachable;
1835 var read_cache = try workspace.acquireReadCache(.{
1836 .pages = options.read_cache_capacity,
1837 });
1838 errdefer workspace.releaseReadCache(&read_cache);
1839 read_cache.activate();
1840 var digest_memo = try workspace.acquireDigestMemo();
1841 errdefer workspace.releaseDigestMemo(&digest_memo);
1842 try options.control.check();
1843
1844 var publication_state = try PublicationState.init(
1845 &path_storage,
1846 options.publication,
1847 );
1848 errdefer if (publication_state) |*state| state.deinit();
1849 var active_paths = options.paths;
1850 var active_lane: ?publication.Lane = null;
1851 if (publication_state) |*state| {
1852 try options.control.check();
1853 if (state.selected) |token| {
1854 var source = try publication.openSelected(
1855 options.io,
1856 dir,
1857 state.basePair(),
1858 token,
1859 );
1860 defer source.deinit(options.io);
1861 state.candidate_lane = source.lane ^ 1;
1862 active_lane = try publication.Lane.init(
1863 state.basePair(),
1864 state.candidate_lane,
1865 );
1866 try publication.prepareOpened(options.io, dir, &source, &active_lane.?);
1867 } else {
1868 try publication.reset(options.io, dir, state.basePair());
1869 active_lane = try publication.Lane.init(
1870 state.basePair(),
1871 state.candidate_lane,
1872 );
1873 }
1874 const pair = active_lane.?.pair();
1875 active_paths = .{ .database = pair.database, .wal = pair.wal };
1876 state.phase = .candidate;
1877 }
1878 try options.control.check();
1879
1880 var recovered = try recoverState(allocator, workspace, dir, .{
1881 .io = options.io,
1882 .paths = active_paths,
1883 .header = options.header,
1884 .max_wal_bytes = options.max_wal_bytes,
1885 .wal_capacity_bytes = wal_capacity_bytes,
1886 .control = options.control,
1887 });
1888 errdefer recovered.deinit();
1889 try options.control.check();
1890 if (recovered.rewrite_base) {
1891 try writeBase(options.io, dir, active_paths.database, &recovered.pager);
1892 recovered.pager.releaseDurableBase();
1893 recovered.base_loaded = false;
1894 }
1895 if (recovered.rewrite_wal) try writeWal(options.io, dir, active_paths.wal, &recovered.pager);
1896 try options.control.check();
1897
1898 var owned_dir = try dir.openDir(options.io, ".", .{});
1899 errdefer owned_dir.close(options.io);
1900 var base_file = try dir.createFile(options.io, active_paths.database, .{ .read = true, .truncate = false });
1901 errdefer base_file.close(options.io);
1902 var wal_file = try dir.createFile(options.io, active_paths.wal, .{ .read = true, .truncate = false });
1903 errdefer wal_file.close(options.io);
1904 const stored_paths = path_storage.storeCurrent(active_paths) catch unreachable;
1905 var transaction_staging = try workspace.acquireTransactionStaging(.{
1906 .frames = try walFrameCapacity(wal_capacity_bytes),
1907 });
1908 errdefer workspace.releaseTransactionStaging(&transaction_staging);
1909 transaction_staging.activate();
1910 trace.progress("file.database.open.complete");
1911 return .{
1912 .allocator = allocator,
1913 .workspace = workspace,
1914 .io = options.io,
1915 .dir = owned_dir,
1916 .path_storage = path_storage,
1917 .paths = stored_paths,
1918 .base_file = base_file,
1919 .wal_file = wal_file,
1920 .pager = recovered.pager,
1921 .wal_written = recovered.pager.walBytes().len,
1922 .max_wal_bytes = options.max_wal_bytes,
1923 .base_loaded = recovered.base_loaded,
1924 .read_cache = read_cache,
1925 .digest_memo = digest_memo,
1926 .read_leases = try ReadLeaseRegistry.init(.{
1927 .leases = options.read_lease_limit,
1928 }),
1929 .transaction_staging = transaction_staging,
1930 .write_capacity = options.write_capacity,
1931 .publication_state = publication_state,
1932 };
1933 }
1934
1935 pub fn openPublishedReadOnly(
1936 allocator: Allocator,
1937 workspace: *Workspace,
1938 dir: std.Io.Dir,
1939 options: PublishedReadOpenOptions,
1940 ) Error!Database {
1941 const path_storage_limits = try PathStorage.Limits.forPublished(options.base_paths);
1942 var path_storage = try workspace.acquirePathStorage(path_storage_limits);
1943 errdefer workspace.releasePathStorage(&path_storage) catch unreachable;
1944 var read_cache = try workspace.acquireReadCache(.{
1945 .pages = options.read_cache_capacity,
1946 });
1947 errdefer workspace.releaseReadCache(&read_cache);
1948 read_cache.activate();
1949 var digest_memo = try workspace.acquireDigestMemo();
1950 errdefer workspace.releaseDigestMemo(&digest_memo);
1951 var opened = try publication.openSelected(options.io, dir, .{
1952 .database = options.base_paths.database,
1953 .wal = options.base_paths.wal,
1954 }, options.token);
1955 var opened_owned = true;
1956 errdefer if (opened_owned) opened.deinit(options.io);
1957 const lane = try publication.Lane.init(.{
1958 .database = options.base_paths.database,
1959 .wal = options.base_paths.wal,
1960 }, opened.lane);
1961 const pair = lane.pair();
1962 var recovered = switch (try recoverReadOnlyFiles(
1963 allocator,
1964 workspace,
1965 opened.database,
1966 opened.wal,
1967 .{
1968 .io = options.io,
1969 .paths = .{ .database = pair.database, .wal = pair.wal },
1970 .header = options.header,
1971 .max_wal_bytes = options.max_wal_bytes,
1972 .read_cache_capacity = options.read_cache_capacity,
1973 },
1974 )) {
1975 .ready => |ready| ready,
1976 .repair_required => return error.InvalidPublication,
1977 };
1978 errdefer workspace.release(&recovered);
1979 var owned_dir = try dir.openDir(options.io, ".", .{});
1980 errdefer owned_dir.close(options.io);
1981 const stored_paths = path_storage.storeCurrent(.{
1982 .database = pair.database,
1983 .wal = pair.wal,
1984 }) catch unreachable;
1985 var transaction_staging = try workspace.acquireTransactionStaging(.{ .frames = 0 });
1986 errdefer workspace.releaseTransactionStaging(&transaction_staging);
1987 transaction_staging.activate();
1988 opened_owned = false;
1989 return .{
1990 .allocator = allocator,
1991 .workspace = workspace,
1992 .io = options.io,
1993 .dir = owned_dir,
1994 .path_storage = path_storage,
1995 .paths = stored_paths,
1996 .base_file = opened.database,
1997 .wal_file = opened.wal,
1998 .pager = recovered,
1999 .wal_written = recovered.walBytes().len,
2000 .max_wal_bytes = options.max_wal_bytes,
2001 .base_loaded = false,
2002 .read_cache = read_cache,
2003 .digest_memo = digest_memo,
2004 .read_leases = try ReadLeaseRegistry.init(.{
2005 .leases = options.read_lease_limit,
2006 }),
2007 .transaction_staging = transaction_staging,
2008 .writable = false,
2009 };
2010 }
2011
2012 pub fn openPublishedReadOnlyForTesting(
2013 allocator: Allocator,
2014 dir: std.Io.Dir,
2015 options: PublishedReadOpenOptions,
2016 ) Error!Database {
2017 if (!builtin.is_test) @compileError("openPublishedReadOnlyForTesting is available only in tests");
2018 const workspace = try allocator.create(Workspace);
2019 errdefer allocator.destroy(workspace);
2020 const path_storage_limits = try PathStorage.Limits.forPublished(options.base_paths);
2021 workspace.* = try Workspace.allocate(allocator, .{
2022 .header = options.header,
2023 .max_wal_bytes = options.max_wal_bytes,
2024 .path_storage = path_storage_limits,
2025 .read_cache_pages = options.read_cache_capacity,
2026 });
2027 errdefer workspace.deallocate(allocator);
2028 var database = try openPublishedReadOnly(allocator, workspace, dir, options);
2029 database.ownWorkspaceForTesting();
2030 return database;
2031 }
2032
2033 pub fn openReadOnly(
2034 allocator: Allocator,
2035 workspace: *Workspace,
2036 dir: std.Io.Dir,
2037 options: ReadOpenOptions,
2038 ) Error!Database {
2039 const path_storage_limits = PathStorage.Limits.forDirect(options.paths);
2040 var path_storage = try workspace.acquirePathStorage(path_storage_limits);
2041 errdefer workspace.releasePathStorage(&path_storage) catch unreachable;
2042 var read_cache = try workspace.acquireReadCache(.{
2043 .pages = options.read_cache_capacity,
2044 });
2045 errdefer workspace.releaseReadCache(&read_cache);
2046 read_cache.activate();
2047 var digest_memo = try workspace.acquireDigestMemo();
2048 errdefer workspace.releaseDigestMemo(&digest_memo);
2049 var base_file = try dir.openFile(options.io, options.paths.database, .{
2050 .allow_directory = false,
2051 });
2052 errdefer base_file.close(options.io);
2053 var wal_file = try dir.openFile(options.io, options.paths.wal, .{
2054 .allow_directory = false,
2055 });
2056 errdefer wal_file.close(options.io);
2057 var recovered = switch (try recoverReadOnlyFiles(
2058 allocator,
2059 workspace,
2060 base_file,
2061 wal_file,
2062 options,
2063 )) {
2064 .ready => |ready| ready,
2065 .repair_required => return error.InvalidDatabaseFile,
2066 };
2067 errdefer workspace.release(&recovered);
2068 var owned_dir = try dir.openDir(options.io, ".", .{});
2069 errdefer owned_dir.close(options.io);
2070 const stored_paths = path_storage.storeCurrent(options.paths) catch unreachable;
2071 var transaction_staging = try workspace.acquireTransactionStaging(.{ .frames = 0 });
2072 errdefer workspace.releaseTransactionStaging(&transaction_staging);
2073 transaction_staging.activate();
2074 return .{
2075 .allocator = allocator,
2076 .workspace = workspace,
2077 .io = options.io,
2078 .dir = owned_dir,
2079 .path_storage = path_storage,
2080 .paths = stored_paths,
2081 .base_file = base_file,
2082 .wal_file = wal_file,
2083 .pager = recovered,
2084 .wal_written = recovered.walBytes().len,
2085 .max_wal_bytes = options.max_wal_bytes,
2086 .base_loaded = false,
2087 .read_cache = read_cache,
2088 .digest_memo = digest_memo,
2089 .read_leases = try ReadLeaseRegistry.init(.{
2090 .leases = options.read_lease_limit,
2091 }),
2092 .transaction_staging = transaction_staging,
2093 .writable = false,
2094 };
2095 }
2096
2097 pub fn openReadOnlyForTesting(
2098 allocator: Allocator,
2099 dir: std.Io.Dir,
2100 options: ReadOpenOptions,
2101 ) Error!Database {
2102 if (!builtin.is_test) @compileError("openReadOnlyForTesting is available only in tests");
2103 const workspace = try allocator.create(Workspace);
2104 errdefer allocator.destroy(workspace);
2105 workspace.* = try Workspace.allocate(allocator, .{
2106 .header = options.header,
2107 .max_wal_bytes = options.max_wal_bytes,
2108 .path_storage = PathStorage.Limits.forDirect(options.paths),
2109 .read_cache_pages = options.read_cache_capacity,
2110 });
2111 errdefer workspace.deallocate(allocator);
2112 var database = try openReadOnly(allocator, workspace, dir, options);
2113 database.ownWorkspaceForTesting();
2114 return database;
2115 }
2116
2117 /// Closes the database and returns everything it borrowed back to the
2118 /// workspace, releasing the read lease registry, closing the base file, the
2119 /// log file, and the directory handle, and returning the pager, the read
2120 /// cache, the transaction staging area, and the path storage. The call
2121 /// frees the cached tree roots through the allocator that `open` was given,
2122 /// and requires that no write transaction is open, which it asserts.
2123 pub fn deinit(self: *Database) void {
2124 const allocator = self.allocator;
2125 const workspace = self.workspace;
2126 const testing_workspace_owned = if (builtin.is_test)
2127 self.testing_workspace_owned
2128 else
2129 false;
2130 std.debug.assert(!self.write_transaction_open);
2131 self.read_leases.deinit();
2132 self.base_file.close(self.io);
2133 self.wal_file.close(self.io);
2134 self.dir.close(self.io);
2135 self.workspace.release(&self.pager);
2136 self.workspace.releaseReadCache(&self.read_cache);
2137 self.workspace.releaseDigestMemo(&self.digest_memo);
2138 self.workspace.releaseTransactionStaging(&self.transaction_staging);
2139 self.tree_roots.deinit(self.allocator);
2140 if (self.publication_state) |*state| state.deinit();
2141 self.workspace.releasePathStorage(&self.path_storage) catch
2142 @panic("invalid database path-storage loan");
2143 self.* = undefined;
2144 if (testing_workspace_owned) {
2145 workspace.deallocate(allocator);
2146 allocator.destroy(workspace);
2147 }
2148 }
2149
2150 pub fn reserve(self: *Database, capacity: pager.Capacity) Error!void {
2151 try self.ensureUsable();
2152 try self.ensureMutable();
2153 try self.pager.reserve(capacity);
2154 }
2155
2156 pub fn beginRead(self: *Database) Error!ReadLease {
2157 try self.ensureUsable();
2158 const index = try self.read_leases.available();
2159 var base_file = try self.leaseBaseFile();
2160 errdefer base_file.deinit(self.io);
2161 const view = (try self.pager.beginRead()).view;
2162 const serial = try self.read_leases.install(
2163 index,
2164 base_file,
2165 view,
2166 self.base_epoch,
2167 );
2168 return .{ .database = self, .index = index, .serial = serial };
2169 }
2170
2171 /// Returns the base file handle for a new read lease. A lane switch
2172 /// closes `base_file` while older leases still read, so a database that
2173 /// publishes through lanes gives each lease its own handle. Any other
2174 /// database keeps `base_file` open until `deinit`, which every lease ends
2175 /// before, so its leases share it.
2176 fn leaseBaseFile(self: *Database) Error!ReadLeaseFile {
2177 if (self.publication_state == null) return .{ .borrowed = &self.base_file };
2178 return .{ .owned = try self.dir.openFile(self.io, self.paths.database, .{}) };
2179 }
2180
2181 pub fn savepoint(self: *Database) Error!Savepoint {
2182 try self.ensureUsable();
2183 if (self.write_transaction_open) return error.WriteTransactionOpen;
2184 return .{
2185 .database = self,
2186 .position = self.pager.position(),
2187 .epoch = self.pager.restoreEpoch(),
2188 .wal_written = self.wal_written,
2189 .state_serial = self.state_serial,
2190 .write_serial = self.write_serial,
2191 };
2192 }
2193
2194 pub fn restore(self: *Database, point: Savepoint) Error!void {
2195 try self.ensureUsable();
2196 try self.ensureMutable();
2197 if (point.database != self) return error.TransactionConflict;
2198 if (self.write_transaction_open) return error.WriteTransactionOpen;
2199 if (self.state_serial == point.state_serial) {
2200 if (self.write_serial != point.write_serial) {
2201 return error.TransactionConflict;
2202 }
2203 if (!std.meta.eql(self.pager.position(), point.position)) {
2204 return error.TransactionConflict;
2205 }
2206 if (self.wal_written != point.wal_written) return error.TransactionConflict;
2207 return;
2208 }
2209 const expected_serial = std.math.add(u64, point.state_serial, 1) catch
2210 return error.TransactionConflict;
2211 const expected_write = std.math.add(u64, point.write_serial, 1) catch
2212 return error.TransactionConflict;
2213 if (self.state_serial != expected_serial) return error.TransactionConflict;
2214 if (self.write_serial != expected_write) return error.TransactionConflict;
2215 if (!self.pager.canRestore(point.position, point.epoch)) {
2216 return error.TransactionConflict;
2217 }
2218 try self.restoreTo(point);
2219 }
2220
2221 pub fn restoreWrites(
2222 self: *Database,
2223 before: Savepoint,
2224 after: Savepoint,
2225 ) Error!void {
2226 try self.ensureUsable();
2227 try self.ensureMutable();
2228 if (before.database != self or after.database != self) {
2229 return error.TransactionConflict;
2230 }
2231 if (self.write_transaction_open) return error.WriteTransactionOpen;
2232 if (self.state_serial != after.state_serial or
2233 self.write_serial != after.write_serial or
2234 !std.meta.eql(self.pager.position(), after.position) or
2235 !std.meta.eql(self.pager.restoreEpoch(), after.epoch) or
2236 self.wal_written != after.wal_written)
2237 {
2238 return error.TransactionConflict;
2239 }
2240 const state_delta = std.math.sub(
2241 u64,
2242 after.state_serial,
2243 before.state_serial,
2244 ) catch return error.TransactionConflict;
2245 const write_delta = std.math.sub(
2246 u64,
2247 after.write_serial,
2248 before.write_serial,
2249 ) catch return error.TransactionConflict;
2250 if (state_delta == 0 or state_delta != write_delta) {
2251 return error.TransactionConflict;
2252 }
2253 if (!self.pager.canRestore(before.position, before.epoch)) {
2254 return error.TransactionConflict;
2255 }
2256 try self.restoreTo(before);
2257 }
2258
2259 fn restoreTo(self: *Database, point: Savepoint) Error!void {
2260 if (self.read_leases.active != 0) return error.ActiveReaders;
2261 try self.ensurePublicationCandidate();
2262 self.wal_synced = false;
2263 self.wal_file.setLength(self.io, point.position.journal.len) catch |err|
2264 return self.ioFailure(err);
2265 self.wal_io.resizes += 1;
2266 self.wal_file.sync(self.io) catch |err|
2267 return self.ioFailure(err);
2268 self.wal_synced = true;
2269 self.pager.restore(point.position);
2270 self.wal_written = point.wal_written;
2271 self.state_serial = point.state_serial;
2272 self.write_serial = point.write_serial;
2273 self.read_cache.clearRetainingCapacity();
2274 }
2275
2276 pub fn walCapacityBytes(self: *const Database) usize {
2277 return self.pager.walCapacityBytes();
2278 }
2279
2280 pub fn walIo(self: *const Database) WalIo {
2281 return self.wal_io;
2282 }
2283
2284 /// Opens the database's single write transaction so that a caller can stage
2285 /// pages inside that write scope, reserving the configured write capacity
2286 /// on first use and giving the transaction its staging area. The call
2287 /// reports `WriteTransactionOpen` when one is already open, and reports
2288 /// `UncommittedWalTail` when the log holds frames past the committed end,
2289 /// which an interrupted write leaves. A refusal for capacity or for an
2290 /// uncommitted tail leaves no transaction open, so the caller is free to
2291 /// checkpoint, raise the configured capacity, or reopen and try again.
2292 pub fn beginWrite(self: *Database) Error!Transaction {
2293 const phase = trace.scope("file.database.begin_write");
2294 defer phase.end();
2295 try self.ensureUsable();
2296 try self.ensureMutable();
2297 try self.ensurePublicationCandidate();
2298 if (self.write_transaction_open) return error.WriteTransactionOpen;
2299 if (!self.write_reserved) {
2300 try self.pager.reserve(self.write_capacity);
2301 self.write_reserved = true;
2302 }
2303 const view = try self.pager.currentView();
2304 if (view.end_mark != self.pager.frameCount()) return error.UncommittedWalTail;
2305 const position = self.pager.position();
2306 self.transaction_staging.begin(self.pager.walStagingCapacity(position));
2307 errdefer self.transaction_staging.end();
2308 self.write_transaction_open = true;
2309 return .{
2310 .database = self,
2311 .start_position = position,
2312 .start_generation = self.pager.baseGeneration(),
2313 .staging = &self.transaction_staging,
2314 };
2315 }
2316
2317 fn appendWal(self: *Database, page_id: u32, db_page_count: u32, image: *const [page.size]u8) Error!void {
2318 const phase = trace.scope("file.database.append_wal");
2319 defer phase.end();
2320 try self.ensureWalFrames(1);
2321 const position = self.pager.position();
2322 const wal_written = self.wal_written;
2323 errdefer self.restoreAppendState(position, wal_written) catch self.poison();
2324 try self.pager.appendWal(page_id, db_page_count, image);
2325 self.persistWal(position.journal.len) catch |err|
2326 return self.ioFailure(err);
2327 trace.progress("file.database.append_wal.complete");
2328 }
2329
2330 fn appendWalSync(self: *Database, page_id: u32, db_page_count: u32, image: *const [page.size]u8) Error!void {
2331 try self.appendWal(page_id, db_page_count, image);
2332 try self.syncWal();
2333 }
2334
2335 pub fn syncWal(self: *Database) Error!void {
2336 const phase = trace.scope("file.database.sync_wal");
2337 defer phase.end();
2338 try self.ensureUsable();
2339 if (self.wal_synced) return;
2340 try self.ensureMutable();
2341 self.wal_file.sync(self.io) catch |err|
2342 return self.ioFailure(err);
2343 self.wal_io.syncs += 1;
2344 self.wal_synced = true;
2345 trace.progress("file.database.sync_wal.complete");
2346 }
2347
2348 pub fn walSynced(self: *const Database) bool {
2349 return self.wal_synced;
2350 }
2351
2352 /// Copies committed pages from the write-ahead log into the base file,
2353 /// which keeps the log from growing without bound. The call reports
2354 /// `RecoveryRequired` while a coordinator holds the database, and reports
2355 /// `ActiveReaders` when a read lease is still holding the current base
2356 /// generation, checking that condition before anything is written. When
2357 /// leases from earlier generations are live, the checkpoint keeps older
2358 /// readers working by holding back the pages they still need. The operation
2359 /// rewrites the log when the checkpoint restarted it. A failure to write
2360 /// the files poisons the database with `RecoveryRequired`.
2361 pub fn checkpoint(self: *Database, options: CheckpointOptions) Error!pager.Checkpoint {
2362 const phase = trace.scope("file.database.checkpoint");
2363 defer phase.end();
2364 try self.ensureUsable();
2365 try options.control.check();
2366 try self.ensureMutable();
2367 try self.ensurePublicationCandidate();
2368 if (self.coordinator_active) return error.RecoveryRequired;
2369 if (self.read_leases.hasEpoch(self.base_epoch)) return error.ActiveReaders;
2370 const oldest = self.read_leases.oldest();
2371 try self.advanceStateSerial();
2372 var prepared = try self.pager.prepareCheckpoint(.{
2373 .readers = if (oldest) |view| .{ .oldest = view } else .none,
2374 .restart_header = options.restart_header,
2375 });
2376 defer prepared.deinit();
2377 try options.control.check();
2378 const has_readers = oldest != null;
2379 const result = if (has_readers) result: {
2380 const committed = try self.pager.commitCheckpoint(prepared);
2381 self.writePreparedCheckpoint(prepared) catch |err|
2382 return self.ioFailure(err);
2383 try options.control.check();
2384 break :result committed;
2385 } else result: {
2386 self.writePreparedCheckpoint(prepared) catch |err|
2387 return self.ioFailure(err);
2388 try options.control.check();
2389 const committed = self.pager.commitDurableCheckpoint(prepared) catch |err|
2390 return self.ioFailure(err);
2391 self.base_loaded = false;
2392 break :result committed;
2393 };
2394 try options.control.check();
2395 if (result.restarted) {
2396 self.rewriteWal() catch |err|
2397 return self.ioFailure(err);
2398 }
2399 try options.control.check();
2400 trace.progress("file.database.checkpoint.complete");
2401 return result;
2402 }
2403
2404 fn writePreparedCheckpoint(self: *Database, prepared: pager.PreparedCheckpoint) Error!void {
2405 const result = prepared.result();
2406 if (result.pages == 0) return;
2407 std.debug.assert(prepared.checkpointPageCount() == result.pages);
2408 try writeCheckpointPages(self.io, self.base_file, prepared, self.pager.databasePageCount());
2409 self.read_cache.clearRetainingCapacity();
2410 }
2411
2412 pub fn flush(self: *Database) Error!void {
2413 const phase = trace.scope("file.database.flush");
2414 defer phase.end();
2415 try self.ensureUsable();
2416 try self.ensureMutable();
2417 if (self.read_leases.active != 0) return error.ActiveReaders;
2418 try self.ensurePublicationCandidate();
2419 if (self.coordinator_active) return error.RecoveryRequired;
2420 try self.advanceStateSerial();
2421 try self.ensureBaseLoaded();
2422 writeBaseFile(self.io, self.base_file, &self.pager) catch |err|
2423 return self.ioFailure(err);
2424 self.rewriteWal() catch |err|
2425 return self.ioFailure(err);
2426 self.pager.releaseDurableBase();
2427 self.base_loaded = false;
2428 self.read_cache.clearRetainingCapacity();
2429 trace.progress("file.database.flush.complete");
2430 }
2431
2432 pub fn beginCoordinator(self: *Database) Error!void {
2433 try self.ensureUsable();
2434 try self.ensureMutable();
2435 if (self.coordinator_active or self.write_transaction_open) {
2436 return error.RecoveryRequired;
2437 }
2438 try self.ensurePublicationCandidate();
2439 self.coordinator_active = true;
2440 }
2441
2442 pub fn endCoordinator(self: *Database) void {
2443 std.debug.assert(self.coordinator_active);
2444 self.coordinator_active = false;
2445 }
2446
2447 pub fn poison(self: *Database) void {
2448 self.recovery_required = true;
2449 }
2450
2451 pub fn requiresRecovery(self: *const Database) bool {
2452 return self.recovery_required;
2453 }
2454
2455 pub fn preparePublication(self: *Database) Error!publication.Token {
2456 try self.ensureUsable();
2457 try self.ensureMutable();
2458 if (self.write_transaction_open or self.coordinator_active) {
2459 return error.RecoveryRequired;
2460 }
2461 const state = if (self.publication_state) |*value|
2462 value
2463 else
2464 return error.InvalidPublication;
2465 if (state.phase != .candidate) return error.InvalidPublication;
2466 self.syncWal() catch |err| return self.ioFailure(err);
2467 self.base_file.sync(self.io) catch |err|
2468 return self.ioFailure(err);
2469 const lane = try publication.Lane.init(state.basePair(), state.candidate_lane);
2470 if (!std.mem.eql(u8, self.paths.database, lane.pair().database) or
2471 !std.mem.eql(u8, self.paths.wal, lane.pair().wal))
2472 {
2473 return error.InvalidPublication;
2474 }
2475 publication.seal(self.io, self.dir, &lane, state.candidate) catch |err|
2476 return self.ioFailure(err);
2477 state.phase = .sealed;
2478 return state.candidate;
2479 }
2480
2481 pub fn publicationCommitted(self: *Database, next: publication.Token) void {
2482 std.debug.assert(self.writable);
2483 std.debug.assert(!self.recovery_required);
2484 const state = if (self.publication_state) |*value| value else unreachable;
2485 std.debug.assert(state.phase == .sealed);
2486 std.debug.assert(next != 0);
2487 std.debug.assert(next != state.candidate);
2488 state.selected = state.candidate;
2489 state.candidate = next;
2490 state.candidate_lane ^= 1;
2491 state.phase = .selected;
2492 }
2493
2494 fn ensureUsable(self: *const Database) Error!void {
2495 if (self.recovery_required) return error.RecoveryRequired;
2496 }
2497
2498 fn ensureMutable(self: *const Database) Error!void {
2499 if (!self.writable) return error.ReadOnlyDatabase;
2500 }
2501
2502 fn ioFailure(self: *Database, err: Error) Error {
2503 if (!self.coordinator_active) self.poison();
2504 return err;
2505 }
2506
2507 fn ensurePublicationCandidate(self: *Database) Error!void {
2508 const state = if (self.publication_state) |*value| value else return;
2509 switch (state.phase) {
2510 .candidate => return,
2511 .sealed => return error.RecoveryRequired,
2512 .selected => {},
2513 }
2514 if (self.write_transaction_open or self.coordinator_active) {
2515 return error.RecoveryRequired;
2516 }
2517 const lane = try publication.Lane.init(state.basePair(), state.candidate_lane);
2518 const pair = lane.pair();
2519 std.debug.assert(self.path_storage.admitsCurrent(.{
2520 .database = pair.database,
2521 .wal = pair.wal,
2522 }));
2523 publication.prepare(self.io, self.dir, self.currentPair(), &lane) catch |err|
2524 return self.ioFailure(err);
2525 self.openPublicationLane(&lane) catch |err| return self.ioFailure(err);
2526 state.phase = .candidate;
2527 }
2528
2529 fn openPublicationLane(self: *Database, lane: *const publication.Lane) Error!void {
2530 const next_epoch = std.math.add(u64, self.base_epoch, 1) catch
2531 return error.GenerationOverflow;
2532 const pair = lane.pair();
2533 const base_file = try self.dir.openFile(self.io, pair.database, .{ .mode = .read_write });
2534 errdefer base_file.close(self.io);
2535 const wal_file = try self.dir.openFile(self.io, pair.wal, .{ .mode = .read_write });
2536 const stored_paths = self.path_storage.storeCurrent(.{
2537 .database = pair.database,
2538 .wal = pair.wal,
2539 }) catch unreachable;
2540 self.base_file.close(self.io);
2541 self.wal_file.close(self.io);
2542 self.paths = stored_paths;
2543 self.base_file = base_file;
2544 self.wal_file = wal_file;
2545 self.base_epoch = next_epoch;
2546 self.read_cache.clearRetainingCapacity();
2547 }
2548
2549 fn currentPair(self: *const Database) publication.Pair {
2550 return .{ .database = self.paths.database, .wal = self.paths.wal };
2551 }
2552
2553 fn rewriteWal(self: *Database) Error!void {
2554 if (self.pager.walBytes().len > self.max_wal_bytes) return error.WalLimitExceeded;
2555 self.wal_synced = false;
2556 try writeWal(self.io, self.dir, self.paths.wal, &self.pager);
2557 const reopened = try self.dir.createFile(self.io, self.paths.wal, .{ .read = true, .truncate = false });
2558 self.wal_file.close(self.io);
2559 self.wal_file = reopened;
2560 self.wal_written = self.pager.walBytes().len;
2561 self.wal_synced = true;
2562 }
2563
2564 fn appendStagedTransactionWal(self: *Database, position: pager.Pager.Position, count: usize, database_page_count: u32) Error!void {
2565 try self.ensureWalFrames(count);
2566 const wal_written = self.wal_written;
2567 errdefer self.restoreAppendState(position, wal_written) catch self.poison();
2568 try self.pager.commitStagedWal(position, count, database_page_count);
2569 self.persistWal(position.journal.len) catch |err|
2570 return self.ioFailure(err);
2571 }
2572
2573 fn ensureWalFrames(self: *const Database, count: usize) Error!void {
2574 const appended = std.math.mul(usize, count, wal.frame_size) catch return error.TransactionTooLarge;
2575 const minimum = std.math.add(usize, wal.header_size, appended) catch return error.TransactionTooLarge;
2576 if (minimum > self.max_wal_bytes) return error.TransactionTooLarge;
2577 const projected = std.math.add(usize, self.pager.walBytes().len, appended) catch return error.TransactionTooLarge;
2578 if (projected > self.max_wal_bytes) return error.WalLimitExceeded;
2579 }
2580
2581 fn persistWal(self: *Database, start: usize) Error!void {
2582 const bytes = self.pager.walBytes();
2583 std.debug.assert(start == self.wal_written);
2584 std.debug.assert(start <= bytes.len);
2585 self.wal_synced = false;
2586 try self.wal_file.writePositionalAll(self.io, bytes[start..], start);
2587 self.wal_io.writes += 1;
2588 self.wal_written = bytes.len;
2589 }
2590
2591 fn restoreAppendState(
2592 self: *Database,
2593 position: pager.Pager.Position,
2594 wal_written: usize,
2595 ) Error!void {
2596 self.pager.restore(position);
2597 try self.wal_file.setLength(self.io, position.journal.len);
2598 self.wal_io.resizes += 1;
2599 self.wal_written = wal_written;
2600 }
2601
2602 fn advanceStateSerial(self: *Database) Error!void {
2603 self.state_serial = std.math.add(u64, self.state_serial, 1) catch
2604 return error.GenerationOverflow;
2605 }
2606
2607 fn advanceWriteSerial(self: *Database) Error!void {
2608 const state_serial = std.math.add(u64, self.state_serial, 1) catch
2609 return error.GenerationOverflow;
2610 const write_serial = std.math.add(u64, self.write_serial, 1) catch
2611 return error.GenerationOverflow;
2612 self.state_serial = state_serial;
2613 self.write_serial = write_serial;
2614 }
2615
2616 fn ensureBaseLoaded(self: *Database) Error!void {
2617 if (self.base_loaded) return;
2618 const count = self.pager.basePageCount();
2619 const generation = self.pager.baseGeneration();
2620 var page_id: u32 = 1;
2621 while (page_id <= count) : (page_id += 1) _ = try self.loadBasePage(page_id, generation);
2622 self.base_loaded = true;
2623 }
2624
2625 fn loadBasePage(self: *Database, page_id: u32, generation: u64) Error!?[]const u8 {
2626 if (page_id == 0 or generation == 0 or page_id > self.pager.basePageCount()) return null;
2627 if (try self.pager.pageAt(page_id, .{ .base_generation = generation, .end_mark = 0 })) |bytes| return bytes;
2628 var image: [page.size]u8 = undefined;
2629 if (!(try self.copyBasePage(page_id, generation, &image))) return null;
2630 try self.pager.installBaseAtGeneration(page_id, &image, generation);
2631 return try self.pager.pageAt(page_id, .{ .base_generation = generation, .end_mark = 0 });
2632 }
2633
2634 fn copyBasePage(self: *Database, page_id: u32, generation: u64, image: *[page.size]u8) Error!bool {
2635 if (page_id == 0) return error.InvalidPageId;
2636 if (generation == 0 or page_id > self.pager.basePageCount()) return false;
2637 const key = ReadCacheKey{ .generation = generation, .page_id = page_id };
2638 if (self.read_cache.get(key, image)) return true;
2639 const n = try self.base_file.readPositionalAll(self.io, image[0..], pageOffset(page_id));
2640 if (n != page.size) return error.InvalidDatabaseFile;
2641 self.read_cache.put(key, image) catch |err| switch (err) {
2642 error.CacheDisabled => {},
2643 };
2644 return true;
2645 }
2646 };
2647
2648 pub const ReadRepairReason = enum {
2649 missing_snapshot,
2650 invalid_database,
2651 invalid_wal,
2652 wal_too_large,
2653 };
2654
2655 const SparseFileStamp = struct {
2656 exists: bool = false,
2657 inode: u64 = 0,
2658 size: u64 = 0,
2659 mtime_ns: i128 = 0,
2660
2661 fn fromFile(io: std.Io, file: std.Io.File) Error!SparseFileStamp {
2662 const stat = try file.stat(io);
2663 return .{
2664 .exists = true,
2665 .inode = @intCast(stat.inode),
2666 .size = stat.size,
2667 .mtime_ns = stat.mtime.toNanoseconds(),
2668 };
2669 }
2670
2671 fn fromPath(io: std.Io, dir: std.Io.Dir, path: []const u8) Error!SparseFileStamp {
2672 const stat = dir.statFile(io, path, .{}) catch |err| switch (err) {
2673 error.FileNotFound => return .{},
2674 else => return err,
2675 };
2676 return .{
2677 .exists = true,
2678 .inode = @intCast(stat.inode),
2679 .size = stat.size,
2680 .mtime_ns = stat.mtime.toNanoseconds(),
2681 };
2682 }
2683
2684 fn sameFile(self: SparseFileStamp, other: SparseFileStamp) bool {
2685 return self.exists == other.exists and
2686 (!self.exists or self.inode == other.inode);
2687 }
2688 };
2689
2690 const SparseReadOnlyState = struct {
2691 io: std.Io,
2692 base_file: ?std.Io.File = null,
2693 wal_file: ?std.Io.File = null,
2694 scanner: wal.Scanner,
2695 base_stamp: SparseFileStamp = .{},
2696 wal_stamp: SparseFileStamp = .{},
2697 frame_capacity: usize,
2698 base_page_count: u32 = 0,
2699 logical_page_count: u32 = 0,
2700 generation: u64 = 0,
2701 active: bool = false,
2702 probe: ?*SparseReadProbe,
2703
2704 fn activate(
2705 self: *SparseReadOnlyState,
2706 dir: std.Io.Dir,
2707 paths: Paths,
2708 control: wal.Control,
2709 ) Error!?ReadRepairReason {
2710 std.debug.assert(!self.active);
2711 std.debug.assert(self.base_file == null);
2712 std.debug.assert(self.wal_file == null);
2713 const base_file = dir.openFile(self.io, paths.database, .{}) catch |err| switch (err) {
2714 error.FileNotFound => null,
2715 else => return err,
2716 };
2717 self.base_file = base_file;
2718 const wal_file = dir.openFile(self.io, paths.wal, .{}) catch |err| switch (err) {
2719 error.FileNotFound => null,
2720 else => return err,
2721 };
2722 self.wal_file = wal_file;
2723 if (base_file == null and wal_file == null) return .missing_snapshot;
2724
2725 const base = loadBaseLengthOpen(self.io, base_file) catch |err| switch (err) {
2726 error.InvalidDatabaseFile => return .invalid_database,
2727 else => return err,
2728 };
2729 self.base_page_count = base.count;
2730 self.logical_page_count = base.count;
2731 if (wal_file) |file| {
2732 const stamp = try SparseFileStamp.fromFile(self.io, file);
2733 const extent = switch (try wal.Extent.classify(
2734 stamp.size,
2735 self.frame_capacity,
2736 control,
2737 )) {
2738 .partial_header => return .invalid_wal,
2739 .over_capacity => return .wal_too_large,
2740 .admitted => |admitted| admitted,
2741 };
2742 var header: [wal.header_size]u8 = undefined;
2743 if (try self.readWal(file, &header, 0, control) != header.len) return .invalid_wal;
2744 self.scanner.reset(&header, extent, base.count, control) catch |err| switch (err) {
2745 error.Interrupted => return error.Interrupted,
2746 error.CapacityExceeded => return .wal_too_large,
2747 else => return .invalid_wal,
2748 };
2749 if (try self.scanFrames(file, control)) |reason| return reason;
2750 if (try self.validateCoverage(base, control)) |reason| return reason;
2751 self.wal_stamp = stamp;
2752 self.logical_page_count = self.scanner.progress().logical_database_page_count;
2753 } else if (base.torn_tail) {
2754 return .invalid_database;
2755 }
2756 self.base_stamp = if (base_file) |file|
2757 try SparseFileStamp.fromFile(self.io, file)
2758 else
2759 .{};
2760 self.generation = std.math.add(u64, self.generation, 1) catch
2761 return error.CapacityOverflow;
2762 self.active = true;
2763 return null;
2764 }
2765
2766 fn refresh(
2767 self: *SparseReadOnlyState,
2768 dir: std.Io.Dir,
2769 options: SparseRefreshOptions,
2770 ) Error!SparseRefreshResult {
2771 if (!self.active) return error.RecoveryRequired;
2772 try options.control.check();
2773 const base_stamp = try SparseFileStamp.fromPath(self.io, dir, options.paths.database);
2774 const wal_stamp = try SparseFileStamp.fromPath(self.io, dir, options.paths.wal);
2775 if (std.meta.eql(base_stamp, self.base_stamp) and
2776 std.meta.eql(wal_stamp, self.wal_stamp))
2777 {
2778 return .unchanged;
2779 }
2780 if (std.meta.eql(base_stamp, self.base_stamp) and
2781 wal_stamp.sameFile(self.wal_stamp) and
2782 wal_stamp.size > self.wal_stamp.size)
2783 {
2784 self.generation = std.math.add(u64, self.generation, 1) catch
2785 return error.CapacityOverflow;
2786 self.active = false;
2787 const extent = switch (try wal.Extent.classify(
2788 wal_stamp.size,
2789 self.frame_capacity,
2790 options.control,
2791 )) {
2792 .partial_header => return self.repair(.invalid_wal),
2793 .over_capacity => return self.repair(.wal_too_large),
2794 .admitted => |admitted| admitted,
2795 };
2796 self.scanner.extend(extent, options.control) catch |err| switch (err) {
2797 error.Interrupted => return error.Interrupted,
2798 error.CapacityExceeded => return self.repair(.wal_too_large),
2799 else => return self.repair(.invalid_wal),
2800 };
2801 if (try self.scanFrames(self.wal_file.?, options.control)) |reason| return self.repair(reason);
2802 const base = BaseLength{
2803 .exists = self.base_stamp.exists,
2804 .count = self.base_page_count,
2805 .torn_tail = false,
2806 };
2807 if (try self.validateCoverage(base, options.control)) |reason| return self.repair(reason);
2808 self.wal_stamp = wal_stamp;
2809 self.logical_page_count = self.scanner.progress().logical_database_page_count;
2810 self.active = true;
2811 return .refreshed;
2812 }
2813
2814 const workspace = self.scanner.deinit();
2815 if (self.base_file) |file| file.close(self.io);
2816 if (self.wal_file) |file| file.close(self.io);
2817 self.base_file = null;
2818 self.wal_file = null;
2819 self.active = false;
2820 self.scanner = try wal.Scanner.init(workspace);
2821 if (try self.activate(dir, options.paths, options.control)) |reason| return .{
2822 .repair_required = reason,
2823 };
2824 return .refreshed;
2825 }
2826
2827 fn scanFrames(
2828 self: *SparseReadOnlyState,
2829 file: std.Io.File,
2830 control: wal.Control,
2831 ) Error!?ReadRepairReason {
2832 var consumed: u32 = 0;
2833 while (self.scanner.nextFrame() catch return .invalid_wal) |request| {
2834 std.debug.assert(consumed < self.frame_capacity);
2835 if (try self.readWal(file, request.bytes, request.offset, control) != request.bytes.len) {
2836 return .invalid_wal;
2837 }
2838 const outcome = self.scanner.consumeFrame(control) catch |err| switch (err) {
2839 error.Interrupted => return error.Interrupted,
2840 error.ScannerNotReady => return .invalid_wal,
2841 };
2842 switch (outcome) {
2843 .staged, .committed => {},
2844 .malformed_full_frame => return .invalid_wal,
2845 }
2846 consumed += 1;
2847 }
2848 _ = self.scanner.outcome() catch return .invalid_wal;
2849 return null;
2850 }
2851
2852 fn validateCoverage(
2853 self: *SparseReadOnlyState,
2854 base: BaseLength,
2855 control: wal.Control,
2856 ) Error!?ReadRepairReason {
2857 const progress = self.scanner.progress();
2858 if (progress.logical_database_page_count < base.count) return .invalid_database;
2859 const tail_pages = progress.logical_database_page_count - base.count;
2860 if (tail_pages > progress.committed_ref_count) return .invalid_database;
2861 var offset: u32 = 0;
2862 while (offset < tail_pages) : (offset += 1) {
2863 try control.check();
2864 const committed = self.scanner.findCommitted(
2865 base.count + offset + 1,
2866 control,
2867 ) catch |err| switch (err) {
2868 error.Interrupted => return error.Interrupted,
2869 error.ScannerNotReady => return .invalid_database,
2870 };
2871 if (committed == null) {
2872 return .invalid_database;
2873 }
2874 }
2875 if (base.torn_tail) {
2876 if (base.count == std.math.maxInt(u32)) return .invalid_database;
2877 const torn = self.scanner.findCommitted(
2878 base.count + 1,
2879 control,
2880 ) catch |err| switch (err) {
2881 error.Interrupted => return error.Interrupted,
2882 error.ScannerNotReady => return .invalid_database,
2883 };
2884 if (torn == null) return .invalid_database;
2885 }
2886 return null;
2887 }
2888
2889 fn copyPage(
2890 self: *SparseReadOnlyState,
2891 generation: u64,
2892 page_id: u32,
2893 image: *[page.size]u8,
2894 ) Error!bool {
2895 if (!self.active or generation != self.generation) return error.RecoveryRequired;
2896 if (page_id == 0) return error.InvalidPageId;
2897 if (page_id > self.logical_page_count) return false;
2898 if (self.wal_file) |file| {
2899 const committed = self.scanner.findCommitted(
2900 page_id,
2901 .{},
2902 ) catch |err| switch (err) {
2903 error.Interrupted => return error.Interrupted,
2904 error.ScannerNotReady => return self.invalidate(),
2905 };
2906 if (committed) |ref| {
2907 const request = self.scanner.selectedFrame(ref, .{}) catch |err| switch (err) {
2908 error.Interrupted => return error.Interrupted,
2909 else => return self.invalidate(),
2910 };
2911 if (try self.readWal(file, request.bytes, request.offset, .{}) != request.bytes.len) {
2912 return self.invalidate();
2913 }
2914 self.scanner.verifySelected(ref, image, .{}) catch |err| switch (err) {
2915 error.Interrupted => return error.Interrupted,
2916 else => return self.invalidate(),
2917 };
2918 return true;
2919 }
2920 }
2921 const file = self.base_file orelse return self.invalidate();
2922 const loaded = try file.readPositionalAll(self.io, image, pageOffset(page_id));
2923 if (self.probe) |probe| probe.base_reads += 1;
2924 if (loaded != image.len) return self.invalidate();
2925 return true;
2926 }
2927
2928 fn readWal(
2929 self: *SparseReadOnlyState,
2930 file: std.Io.File,
2931 target: []u8,
2932 offset: u64,
2933 control: wal.Control,
2934 ) Error!usize {
2935 try control.check();
2936 const loaded = try file.readPositionalAll(self.io, target, offset);
2937 if (self.probe) |probe| {
2938 probe.wal_reads += 1;
2939 probe.wal_bytes += loaded;
2940 }
2941 try control.check();
2942 return loaded;
2943 }
2944
2945 fn repair(self: *SparseReadOnlyState, reason: ReadRepairReason) SparseRefreshResult {
2946 self.active = false;
2947 return .{ .repair_required = reason };
2948 }
2949
2950 fn invalidate(self: *SparseReadOnlyState) Error {
2951 self.active = false;
2952 self.generation +%= 1;
2953 return error.RecoveryRequired;
2954 }
2955
2956 fn deinit(self: *SparseReadOnlyState) void {
2957 if (self.base_file) |file| file.close(self.io);
2958 if (self.wal_file) |file| file.close(self.io);
2959 _ = self.scanner.deinit();
2960 self.* = undefined;
2961 }
2962 };
2963
2964 pub const SparseReadOnlyDatabase = struct {
2965 storage: [@sizeOf(SparseReadOnlyState)]u8 align(@alignOf(SparseReadOnlyState)),
2966
2967 pub fn openExisting(
2968 dir: std.Io.Dir,
2969 options: SparseReadOptions,
2970 ) Error!SparseReadOpenResult {
2971 var database: SparseReadOnlyDatabase = undefined;
2972 database.impl().* = .{
2973 .io = options.io,
2974 .scanner = try wal.Scanner.init(options.workspace),
2975 .frame_capacity = options.workspace.refs.len,
2976 .probe = options.probe,
2977 };
2978 errdefer database.deinit();
2979 if (try database.impl().activate(dir, options.paths, options.control)) |reason| {
2980 database.deinit();
2981 return .{ .repair_required = reason };
2982 }
2983 return .{ .ready = database };
2984 }
2985
2986 pub fn deinit(self: *SparseReadOnlyDatabase) void {
2987 self.impl().deinit();
2988 self.* = undefined;
2989 }
2990
2991 pub fn refresh(
2992 self: *SparseReadOnlyDatabase,
2993 dir: std.Io.Dir,
2994 options: SparseRefreshOptions,
2995 ) Error!SparseRefreshResult {
2996 return try self.impl().refresh(dir, options);
2997 }
2998
2999 pub fn snapshot(self: *SparseReadOnlyDatabase) Snapshot {
3000 const state = self.impl();
3001 std.debug.assert(state.active);
3002 return .{
3003 .source = .{ .sparse = .{
3004 .database = self,
3005 .generation = state.generation,
3006 } },
3007 .view = .{
3008 .base_generation = state.generation,
3009 .end_mark = state.scanner.progress().committed_end_mark,
3010 },
3011 };
3012 }
3013
3014 fn impl(self: *SparseReadOnlyDatabase) *SparseReadOnlyState {
3015 return @ptrCast(&self.storage);
3016 }
3017 };
3018
3019 pub const SparseReadOpenResult = union(enum) {
3020 ready: SparseReadOnlyDatabase,
3021 repair_required: ReadRepairReason,
3022 };
3023
3024 pub const SparseRefreshResult = union(enum) {
3025 unchanged,
3026 refreshed,
3027 repair_required: ReadRepairReason,
3028 };
3029
3030 const ReadOnlyState = struct {
3031 allocator: Allocator,
3032 workspace: *DatabaseWorkspace,
3033 io: std.Io,
3034 base_file: ?std.Io.File,
3035 pager: pager.Pager,
3036 read_cache: ReadCache,
3037 view: pager.View,
3038 testing_workspace_owned: if (builtin.is_test) bool else void = if (builtin.is_test) false else {},
3039 };
3040
3041 pub const ReadOnlyDatabase = struct {
3042 pub const Workspace = DatabaseWorkspace;
3043
3044 storage: [@sizeOf(ReadOnlyState)]u8 align(@alignOf(ReadOnlyState)),
3045
3046 pub fn openForTesting(
3047 allocator: Allocator,
3048 dir: std.Io.Dir,
3049 options: ReadOpenOptions,
3050 ) Error!ReadOpenResult {
3051 if (!builtin.is_test) @compileError("openForTesting is available only in tests");
3052 const workspace = try allocator.create(Workspace);
3053 errdefer allocator.destroy(workspace);
3054 workspace.* = try Workspace.allocate(allocator, .{
3055 .header = options.header,
3056 .max_wal_bytes = options.max_wal_bytes,
3057 .path_storage = .{
3058 .database_bytes = 0,
3059 .wal_bytes = 0,
3060 },
3061 .read_cache_pages = options.read_cache_capacity,
3062 });
3063 errdefer workspace.deallocate(allocator);
3064 const opened = try openExisting(allocator, workspace, dir, options);
3065 return switch (opened) {
3066 .ready => |ready| ready: {
3067 var database = ready;
3068 database.impl().testing_workspace_owned = true;
3069 break :ready .{ .ready = database };
3070 },
3071 .repair_required => |reason| repair: {
3072 workspace.deallocate(allocator);
3073 allocator.destroy(workspace);
3074 break :repair .{ .repair_required = reason };
3075 },
3076 };
3077 }
3078
3079 pub fn ownWorkspaceForTesting(self: *ReadOnlyDatabase) void {
3080 if (!builtin.is_test) @compileError("ownWorkspaceForTesting is available only in tests");
3081 const state = self.impl();
3082 std.debug.assert(!state.testing_workspace_owned);
3083 state.testing_workspace_owned = true;
3084 }
3085
3086 pub fn openExisting(
3087 allocator: Allocator,
3088 workspace: *Workspace,
3089 dir: std.Io.Dir,
3090 options: ReadOpenOptions,
3091 ) Error!ReadOpenResult {
3092 try options.control.check();
3093 var read_cache = try workspace.acquireReadCache(.{
3094 .pages = options.read_cache_capacity,
3095 });
3096 errdefer workspace.releaseReadCache(&read_cache);
3097 read_cache.activate();
3098 const recovered = try recoverReadOnly(allocator, workspace, dir, options);
3099 var recovered_pager = switch (recovered) {
3100 .repair_required => |reason| {
3101 workspace.releaseReadCache(&read_cache);
3102 return .{ .repair_required = reason };
3103 },
3104 .ready => |ready| ready,
3105 };
3106 errdefer workspace.release(&recovered_pager);
3107 try options.control.check();
3108 const base_file: ?std.Io.File = if (try filePresent(options.io, dir, options.paths.database))
3109 try dir.openFile(options.io, options.paths.database, .{})
3110 else
3111 null;
3112 errdefer if (base_file) |file| file.close(options.io);
3113 const view = (try recovered_pager.beginRead()).view;
3114 try options.control.check();
3115 var database: ReadOnlyDatabase = undefined;
3116 database.impl().* = .{
3117 .allocator = allocator,
3118 .workspace = workspace,
3119 .io = options.io,
3120 .base_file = base_file,
3121 .pager = recovered_pager,
3122 .read_cache = read_cache,
3123 .view = view,
3124 };
3125 return .{ .ready = database };
3126 }
3127
3128 pub fn deinit(self: *ReadOnlyDatabase) void {
3129 const state = self.impl();
3130 const allocator = state.allocator;
3131 const workspace = state.workspace;
3132 const testing_workspace_owned = if (builtin.is_test)
3133 state.testing_workspace_owned
3134 else
3135 false;
3136 if (state.base_file) |file| file.close(state.io);
3137 state.workspace.release(&state.pager);
3138 state.workspace.releaseReadCache(&state.read_cache);
3139 self.* = undefined;
3140 if (testing_workspace_owned) {
3141 workspace.deallocate(allocator);
3142 allocator.destroy(workspace);
3143 }
3144 }
3145
3146 pub fn snapshot(self: *ReadOnlyDatabase) Snapshot {
3147 const state = self.impl();
3148 return .{
3149 .source = .{ .durable = .{
3150 .io = state.io,
3151 .base_file = if (state.base_file) |*file| file else null,
3152 .pager = &state.pager,
3153 .read_cache = &state.read_cache,
3154 } },
3155 .view = state.view,
3156 };
3157 }
3158
3159 pub fn readCacheCapacity(self: *const ReadOnlyDatabase) usize {
3160 return self.implConst().read_cache.capacity.pages;
3161 }
3162
3163 fn impl(self: *ReadOnlyDatabase) *ReadOnlyState {
3164 return @ptrCast(&self.storage);
3165 }
3166
3167 fn implConst(self: *const ReadOnlyDatabase) *const ReadOnlyState {
3168 return @ptrCast(&self.storage);
3169 }
3170 };
3171
3172 pub const ReadOpenResult = union(enum) {
3173 ready: ReadOnlyDatabase,
3174 repair_required: ReadRepairReason,
3175 };
3176
3177 pub const ReadLease = struct {
3178 database: *Database,
3179 index: u8,
3180 serial: u64,
3181
3182 /// Releases one hold on this read lease. A lease of a database that
3183 /// publishes through lanes keeps its own handle on the base file, and that
3184 /// handle closes when the last hold is released. Other leases share the
3185 /// database's handle. Every snapshot taken from the lease stops being
3186 /// usable once the last hold is gone.
3187 pub fn deinit(self: *ReadLease) void {
3188 self.database.read_leases.release(
3189 self.database.io,
3190 self.index,
3191 self.serial,
3192 );
3193 self.* = undefined;
3194 }
3195
3196 pub fn retain(self: *const ReadLease) Error!ReadLease {
3197 try self.database.read_leases.retain(self.index, self.serial);
3198 return self.*;
3199 }
3200
3201 pub fn snapshot(self: *const ReadLease) Snapshot {
3202 const slot = self.database.read_leases.live(
3203 self.index,
3204 self.serial,
3205 ) catch unreachable;
3206 const base_file = if (slot.base_file) |*value| value.file() else unreachable;
3207 return .{
3208 .source = .{ .durable = .{
3209 .io = self.database.io,
3210 .base_file = base_file,
3211 .pager = &self.database.pager,
3212 .read_cache = &self.database.read_cache,
3213 .recovery_required = &self.database.recovery_required,
3214 .guard = .{
3215 .database = self.database,
3216 .index = self.index,
3217 .serial = self.serial,
3218 },
3219 } },
3220 .view = slot.view,
3221 };
3222 }
3223 };
3224
3225 const ReadGuard = struct {
3226 database: *Database,
3227 index: u8,
3228 serial: u64,
3229
3230 fn valid(self: ReadGuard) bool {
3231 return self.database.read_leases.isLive(self.index, self.serial);
3232 }
3233
3234 fn retain(self: ReadGuard) Error!ReadLease {
3235 try self.database.read_leases.retain(self.index, self.serial);
3236 return .{
3237 .database = self.database,
3238 .index = self.index,
3239 .serial = self.serial,
3240 };
3241 }
3242 };
3243
3244 const DurableSnapshot = struct {
3245 io: std.Io,
3246 base_file: ?*const std.Io.File,
3247 /// Mutable only for the check marks that copies hand out.
3248 pager: *pager.Pager,
3249 read_cache: *ReadCache,
3250 recovery_required: ?*const bool = null,
3251 guard: ?ReadGuard = null,
3252 };
3253
3254 const SparseSnapshot = struct {
3255 database: *SparseReadOnlyDatabase,
3256 generation: u64,
3257 };
3258
3259 const SnapshotSource = union(enum) {
3260 durable: DurableSnapshot,
3261 sparse: SparseSnapshot,
3262 };
3263
3264 /// The check mark of the stored image that a snapshot copy came from. A
3265 /// reader records it once the copy passes every check for its page kind, and
3266 /// a later copy of the same stored image then reports it checked. Storing new
3267 /// bytes under a mark clears it. A copy with no stored image behind it, such
3268 /// as a sparse page or a disk read with the cache disabled, is never checked.
3269 pub const PageMark = struct {
3270 flag: ?*bool,
3271
3272 pub const none: PageMark = .{ .flag = null };
3273
3274 /// Whether a reader recorded the stored image as passing its checks.
3275 pub fn checked(self: PageMark) bool {
3276 const flag = self.flag orelse return false;
3277 return flag.*;
3278 }
3279
3280 /// Records that the copy passed every check for its page kind. The next
3281 /// read from the database may store other bytes under the mark, so a
3282 /// reader records before it reads again.
3283 pub fn record(self: PageMark) void {
3284 if (self.flag) |flag| flag.* = true;
3285 }
3286 };
3287
3288 pub const Snapshot = struct {
3289 source: SnapshotSource,
3290 view: pager.View,
3291
3292 /// Returns the digest of a stored tree identity read through this
3293 /// snapshot. A leased durable snapshot asks its database's memo, and any
3294 /// other snapshot hashes the state directly.
3295 pub fn digestIdentity(
3296 self: Snapshot,
3297 identity_page: u32,
3298 state: *const lattice.State,
3299 entries: u64,
3300 ) [lattice.digest_size]u8 {
3301 std.debug.assert(identity_page != 0);
3302 return switch (self.source) {
3303 .durable => |durable| if (durable.guard) |guard|
3304 guard.database.digest_memo.digest(identity_page, state, entries)
3305 else
3306 state.digest(entries),
3307 .sparse => state.digest(entries),
3308 };
3309 }
3310
3311 pub fn retain(self: Snapshot) Error!?ReadLease {
3312 return switch (self.source) {
3313 .durable => |durable| if (durable.guard) |guard|
3314 try guard.retain()
3315 else
3316 null,
3317 .sparse => null,
3318 };
3319 }
3320
3321 pub fn copyPage(self: Snapshot, page_id: u32, image: *[page.size]u8) Error!bool {
3322 return try self.copyMarkedPage(page_id, image) != null;
3323 }
3324
3325 /// Copies page `page_id` into `image` and returns the check mark of the
3326 /// stored image it copied, or null when the snapshot holds no such page.
3327 pub fn copyMarkedPage(self: Snapshot, page_id: u32, image: *[page.size]u8) Error!?PageMark {
3328 const phase = trace.scope("file.snapshot.copy_page");
3329 defer phase.end();
3330 switch (self.source) {
3331 .durable => |durable| return try copyDurablePage(durable, self.view, page_id, image),
3332 .sparse => |sparse| {
3333 const found = try sparse.database.impl().copyPage(
3334 sparse.generation,
3335 page_id,
3336 image,
3337 );
3338 return if (found) .none else null;
3339 },
3340 }
3341 }
3342
3343 fn copyDurablePage(
3344 durable: DurableSnapshot,
3345 view: pager.View,
3346 page_id: u32,
3347 image: *[page.size]u8,
3348 ) Error!?PageMark {
3349 if (durable.guard) |guard| {
3350 if (!guard.valid()) return error.InvalidReadLease;
3351 }
3352 if (durable.recovery_required) |required| {
3353 if (required.*) return error.RecoveryRequired;
3354 }
3355 if (page_id == 0) return error.InvalidPageId;
3356 if (try durable.pager.markedPageAt(page_id, view)) |stored| {
3357 image.* = stored.bytes.*;
3358 return .{ .flag = stored.checked };
3359 }
3360 if (view.base_generation == 0 or page_id > durable.pager.basePageCount()) return null;
3361 const key = ReadCacheKey{ .generation = view.base_generation, .page_id = page_id };
3362 if (durable.read_cache.get(key, image)) {
3363 return .{ .flag = durable.read_cache.checkMark(key) };
3364 }
3365 const base_file = (durable.base_file orelse return error.InvalidDatabaseFile).*;
3366 const n = try base_file.readPositionalAll(durable.io, image[0..], pageOffset(page_id));
3367 if (n != page.size) return error.InvalidDatabaseFile;
3368 durable.read_cache.put(key, image) catch |err| switch (err) {
3369 error.CacheDisabled => return .none,
3370 };
3371 return .{ .flag = durable.read_cache.checkMark(key) };
3372 }
3373 };
3374
3375 const TransactionIndexEntry = struct {
3376 generation: u32 = 0,
3377 page_index: u32 = 0,
3378 };
3379
3380 pub const TransactionStaging = struct {
3381 pub const storage_alignment: usize = @alignOf(TransactionIndexEntry);
3382 pub const Storage = []align(storage_alignment) u8;
3383
3384 pub const Limits = struct {
3385 frames: usize,
3386 };
3387
3388 pub const Capacity = struct {
3389 frames: usize,
3390 index_slots: usize,
3391 storage_bytes: usize,
3392
3393 pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity {
3394 if (limits.frames > std.math.maxInt(u32)) return error.CapacityOverflow;
3395 const index_slots = std.math.mul(usize, limits.frames, 2) catch return error.CapacityOverflow;
3396 const storage_bytes = std.math.mul(usize, index_slots, @sizeOf(TransactionIndexEntry)) catch return error.CapacityOverflow;
3397 return .{
3398 .frames = limits.frames,
3399 .index_slots = index_slots,
3400 .storage_bytes = storage_bytes,
3401 };
3402 }
3403 };
3404
3405 pub const Exhaustion = error{TransactionTooLarge};
3406 pub const InitError = error{ CapacityOverflow, StorageTooShort };
3407 pub const work_limits: alloc_phase.capacity.WorkLimits = .{
3408 .transition_steps_max = std.math.maxInt(usize),
3409 .cleanup_steps_per_call_max = 0,
3410 .cleanup_calls_at_capacity_max = 0,
3411 };
3412
3413 pub const claim: alloc_phase.capacity.Declaration = .{
3414 .source = .{
3415 .id = "sql.file_transaction_staging",
3416 .kind = .phase_static,
3417 .limit_source = .caller,
3418 .storage = .{
3419 .covered = &.{
3420 .{
3421 .id = "exact_dense_page_lookup_index_for_the_borrowed_wal_bd67af59f1b2",
3422 .lifetime = .steady,
3423 .detail = "exact dense-page lookup index for the borrowed WAL tail frame bound",
3424 },
3425 },
3426 .excluded = &.{
3427 "staged and committed page bytes owned by the phase static WAL writer",
3428 "database pager frame and page indexes base images read cache and tree roots",
3429 "file handles persistence effects transaction metadata and trace instrumentation",
3430 },
3431 },
3432 .capacity = .{
3433 .inputs = &.{
3434 alloc_phase.capacity.bindInput(Limits, "frames", "frames"),
3435 },
3436 .type_selectors = &.{
3437 alloc_phase.capacity.bindType(TransactionIndexEntry, "transaction_index_entry"),
3438 },
3439 .nodes = &.{
3440 .{ .input = 0 },
3441 .{ .scale = .{ .node = 0, .coefficient = .{ .literal = 2 } } },
3442 .{ .scale = .{ .node = 1, .coefficient = .{ .size_of_concrete_type = 0 } } },
3443 },
3444 .assertions = &.{.{
3445 .scope = .closure_total,
3446 .measure = .retained,
3447 .relation = .exact,
3448 .expression = 2,
3449 }},
3450 },
3451 .overload = .{
3452 .kind = .reject_before_mutation,
3453 .detail = "a full remaining WAL frame prefix rejects a new page before the reusable lookup index or borrowed WAL tail mutate",
3454 },
3455 .risks = .{
3456 .transitive = .{
3457 .status = .witnessed,
3458 .detail = "the transaction stages sorts and transfers page images within WAL owned storage without a second page allocation",
3459 },
3460 .foreign = .{
3461 .status = .excluded,
3462 .detail = "WAL file persistence consumes the committed borrowed bytes after the in memory ownership transfer",
3463 },
3464 },
3465 .work = .{
3466 .equation = "begin visits at most twice the admitted frame bound only when the generation wraps; other steady operations take constant steps",
3467 },
3468 .dependencies = &.{"sql.wal_writer"},
3469 .obligations = &.{
3470 .{ .key = "sql_file_transaction_staging_capacity", .role = .capacity_model },
3471 .{ .key = "sql_file_transaction_staging_sealed", .role = .overload },
3472 .{ .key = "sql_file_transaction_staging_work_bound", .role = .work_bound },
3473 .{ .key = "sql_file_transaction_staging_transfer_transitive_risk", .role = .transitive_risk },
3474 .{ .key = "sql_file_transaction_staging_transfer_foreign_risk", .role = .foreign_risk },
3475 },
3476 },
3477 .bindings = .{
3478 .owner = @This(),
3479 .seal = .{
3480 .family = alloc_phase.capacity.selector(@This().activate),
3481 .premise = .{
3482 .class = .checked_semantic_fact,
3483 .authority = .checker,
3484 },
3485 },
3486 .teardown = .{
3487 .family = alloc_phase.capacity.selector(@This().deinit),
3488 .premise = .{
3489 .class = .checked_semantic_fact,
3490 .authority = .checker,
3491 },
3492 },
3493 },
3494 };
3495
3496 phase: alloc_phase.capacity.Phase,
3497 capacity: Capacity,
3498 storage: Storage,
3499 index: []TransactionIndexEntry,
3500 generation: u32 = 0,
3501 limit_frames: usize = 0,
3502 pages: usize = 0,
3503 active: bool = false,
3504
3505 pub fn init(storage: Storage, limits: Limits) InitError!TransactionStaging {
3506 const capacity = try Capacity.derive(limits);
3507 if (storage.len < capacity.storage_bytes) return error.StorageTooShort;
3508 const borrowed = storage[0..capacity.storage_bytes];
3509 const index = std.mem.bytesAsSlice(TransactionIndexEntry, borrowed);
3510 @memset(index, .{});
3511 return .{
3512 .phase = .initialization,
3513 .capacity = capacity,
3514 .storage = borrowed,
3515 .index = index,
3516 };
3517 }
3518
3519 pub fn activate(self: *TransactionStaging) void {
3520 std.debug.assert(self.phase == .initialization);
3521 std.debug.assert(self.index.len == self.capacity.index_slots);
3522 self.phase = .steady;
3523 }
3524
3525 pub fn deinit(self: *TransactionStaging) Storage {
3526 std.debug.assert(self.phase != .teardown);
3527 std.debug.assert(!self.active);
3528 std.debug.assert(self.storage.len == self.capacity.storage_bytes);
3529 std.debug.assert(self.index.len == self.capacity.index_slots);
3530 std.debug.assert(self.index.ptr == std.mem.bytesAsSlice(
3531 TransactionIndexEntry,
3532 self.storage,
3533 ).ptr);
3534 self.phase = .teardown;
3535 const storage = self.storage;
3536 self.* = undefined;
3537 return storage;
3538 }
3539
3540 pub fn begin(self: *TransactionStaging, frames: usize) void {
3541 std.debug.assert(self.phase == .steady);
3542 std.debug.assert(!self.active);
3543 std.debug.assert(frames <= self.capacity.frames);
3544 self.generation +%= 1;
3545 if (self.generation == 0) {
3546 @memset(self.index, .{});
3547 self.generation = 1;
3548 }
3549 self.limit_frames = frames;
3550 self.pages = 0;
3551 self.active = true;
3552 }
3553
3554 pub fn end(self: *TransactionStaging) void {
3555 std.debug.assert(self.phase == .steady);
3556 std.debug.assert(self.active);
3557 self.limit_frames = 0;
3558 self.pages = 0;
3559 self.active = false;
3560 }
3561
3562 pub fn pageIndex(self: *const TransactionStaging, index: usize) ?usize {
3563 std.debug.assert(self.phase == .steady);
3564 std.debug.assert(self.active);
3565 std.debug.assert(index < self.capacity.index_slots);
3566 const entry = self.index[index];
3567 return if (entry.generation == self.generation) entry.page_index else null;
3568 }
3569
3570 pub fn nextPage(self: *const TransactionStaging) Exhaustion!usize {
3571 std.debug.assert(self.phase == .steady);
3572 std.debug.assert(self.active);
3573 if (self.pages >= self.limit_frames) return error.TransactionTooLarge;
3574 return self.pages;
3575 }
3576
3577 pub fn occupy(self: *TransactionStaging, index: usize, page_index: usize) void {
3578 std.debug.assert(self.phase == .steady);
3579 std.debug.assert(self.active);
3580 std.debug.assert(index < self.capacity.index_slots);
3581 std.debug.assert(self.index[index].generation != self.generation);
3582 std.debug.assert(page_index == self.pages);
3583 std.debug.assert(page_index < self.limit_frames);
3584 self.index[index] = .{
3585 .generation = self.generation,
3586 .page_index = @intCast(page_index),
3587 };
3588 self.pages += 1;
3589 }
3590 };
3591
3592 comptime {
3593 alloc_phase.capacity.requireProvisionedRejectingOwnerShape(TransactionStaging);
3594 }
3595
3596 const TransactionSlot = union(enum) {
3597 existing: usize,
3598 empty: usize,
3599 full,
3600 };
3601
3602 pub const Transaction = struct {
3603 database: *Database,
3604 start_position: pager.Pager.Position,
3605 start_generation: u64,
3606 staging: *TransactionStaging,
3607 closed: bool = false,
3608
3609 /// Ends the transaction without committing it, usually called through a
3610 /// defer so that an abandoned transaction gives the database's single write
3611 /// slot back and another transaction can open. The call drops the staged
3612 /// pages, so nothing the transaction put reaches the log, and does nothing
3613 /// on a transaction that has already committed or ended.
3614 pub fn deinit(self: *Transaction) void {
3615 self.close();
3616 }
3617
3618 pub fn putPage(self: *Transaction, page_id: u32, image: *const [page.size]u8) Error!void {
3619 if (self.closed) return error.TransactionClosed;
3620 if (page_id == 0) return error.InvalidPageId;
3621 try self.ensureCurrent();
3622 const phase = trace.scope("file.transaction.put_page");
3623 defer phase.end();
3624 switch (self.findSlot(page_id)) {
3625 .existing => |index| try self.database.pager.stageWalPage(self.start_position, index, page_id, image),
3626 .empty => |index| {
3627 const page_index = self.staging.nextPage() catch return self.capacityError();
3628 try self.database.pager.stageWalPage(self.start_position, page_index, page_id, image);
3629 self.staging.occupy(index, page_index);
3630 },
3631 .full => return self.capacityError(),
3632 }
3633 }
3634
3635 pub fn getPage(self: *const Transaction, page_id: u32) Error!?[]const u8 {
3636 if (self.closed) return error.TransactionClosed;
3637 if (page_id == 0) return error.InvalidPageId;
3638 try self.ensureCurrent();
3639 return switch (self.findSlot(page_id)) {
3640 .existing => |index| self.database.pager.stagedWalPage(self.start_position, index),
3641 .empty => null,
3642 .full => null,
3643 };
3644 }
3645
3646 /// Returns the image this transaction staged for `page_id`, or null when
3647 /// it staged none. Edits through the image are staged as they happen,
3648 /// so putting the same image again copies nothing. The image stays
3649 /// valid until the transaction commits or ends.
3650 pub fn editPage(self: *Transaction, page_id: u32) Error!?*[page.size]u8 {
3651 if (self.closed) return error.TransactionClosed;
3652 if (page_id == 0) return error.InvalidPageId;
3653 try self.ensureCurrent();
3654 return switch (self.findSlot(page_id)) {
3655 .existing => |index| self.database.pager.stagedWalPageMut(self.start_position, index),
3656 .empty => null,
3657 .full => null,
3658 };
3659 }
3660
3661 /// Appends every staged page to the write-ahead log as one committed batch
3662 /// to make those pages part of the database, sorting the staged pages by
3663 /// page number before appending them. Callers choose whether to wait for
3664 /// the bytes to reach the disk through durability options: with `synced`
3665 /// durability the call syncs the log before returning and reports the
3666 /// commit as synced, and with `buffered` it returns as soon as the bytes
3667 /// are written and reports the commit as not yet synced. A transaction with
3668 /// nothing staged commits as a no-op, reporting zero frames and zero pages.
3669 /// The call reports `TransactionConflict` when the database moved
3670 /// underneath the transaction, and `TransactionClosed` when the transaction
3671 /// has already ended. The transaction ends whichever way the call goes, so
3672 /// the write slot is free even after a failure.
3673 pub fn commit(self: *Transaction, options: CommitOptions) Error!Commit {
3674 if (self.closed) return error.TransactionClosed;
3675 defer self.close();
3676 const phase = trace.scope("file.transaction.commit");
3677 defer phase.end();
3678 try self.ensureCurrent();
3679 if (self.staging.pages == 0) {
3680 return .{
3681 .view = try self.database.pager.currentView(),
3682 .frames = 0,
3683 .pages = 0,
3684 .synced = false,
3685 };
3686 }
3687
3688 self.sortPages();
3689 var page_count = self.database.pager.databasePageCount();
3690 for (0..self.staging.pages) |index| page_count = @max(page_count, self.pageId(index));
3691 try self.database.advanceWriteSerial();
3692 try self.database.appendStagedTransactionWal(self.start_position, self.staging.pages, page_count);
3693
3694 const view = try self.database.pager.currentView();
3695 const frames = self.staging.pages;
3696 const synced = switch (options.durability) {
3697 .buffered => false,
3698 .synced => synced: {
3699 try self.database.syncWal();
3700 break :synced true;
3701 },
3702 };
3703 trace.progress("file.transaction.commit.complete");
3704 return .{
3705 .view = view,
3706 .frames = frames,
3707 .pages = frames,
3708 .synced = synced,
3709 };
3710 }
3711
3712 fn close(self: *Transaction) void {
3713 if (self.closed) return;
3714 std.debug.assert(self.database.write_transaction_open);
3715 self.staging.end();
3716 self.database.write_transaction_open = false;
3717 self.closed = true;
3718 }
3719
3720 fn ensureCurrent(self: *const Transaction) Error!void {
3721 try self.database.ensureUsable();
3722 if (!std.meta.eql(self.database.pager.position(), self.start_position) or self.database.pager.baseGeneration() != self.start_generation) {
3723 return error.TransactionConflict;
3724 }
3725 }
3726
3727 fn findSlot(self: *const Transaction, page_id: u32) TransactionSlot {
3728 const slots = self.staging.capacity.index_slots;
3729 if (slots == 0) return .full;
3730 var index = std.hash.Wyhash.hash(0x7469_6e79_7371_6c31, std.mem.asBytes(&page_id)) % slots;
3731 for (0..slots) |_| {
3732 const page_index = self.staging.pageIndex(index) orelse return .{ .empty = index };
3733 if (self.database.pager.stagedWalPageId(self.start_position, page_index) == page_id) return .{ .existing = page_index };
3734 index = if (index + 1 == slots) 0 else index + 1;
3735 }
3736 return .full;
3737 }
3738
3739 fn capacityError(self: *const Transaction) DatabaseFileError {
3740 return if (self.staging.limit_frames < self.staging.capacity.frames) error.WalLimitExceeded else error.TransactionTooLarge;
3741 }
3742
3743 fn sortPages(self: *Transaction) void {
3744 if (self.staging.pages < 2) return;
3745 self.quickSortPages(0, self.staging.pages);
3746 }
3747
3748 fn quickSortPages(self: *Transaction, start: usize, end: usize) void {
3749 if (end - start < 16) return self.insertionSortPages(start, end);
3750 const pivot = self.pageId(start + (end - start) / 2);
3751 var left = start;
3752 var right = end - 1;
3753 while (left <= right) {
3754 while (self.pageId(left) < pivot) left += 1;
3755 while (self.pageId(right) > pivot) {
3756 if (right == start) break;
3757 right -= 1;
3758 }
3759 if (left <= right) {
3760 self.database.pager.swapStagedWalFrames(self.start_position, left, right);
3761 left += 1;
3762 if (right == start) break;
3763 right -= 1;
3764 }
3765 }
3766 if (right > start) self.quickSortPages(start, right + 1);
3767 if (left < end) self.quickSortPages(left, end);
3768 }
3769
3770 fn insertionSortPages(self: *Transaction, start: usize, end: usize) void {
3771 var index = start + 1;
3772 while (index < end) : (index += 1) {
3773 var scan = index;
3774 while (scan > start and self.pageId(scan) < self.pageId(scan - 1)) : (scan -= 1) {
3775 self.database.pager.swapStagedWalFrames(self.start_position, scan, scan - 1);
3776 }
3777 }
3778 }
3779
3780 fn pageId(self: *const Transaction, index: usize) u32 {
3781 return self.database.pager.stagedWalPageId(self.start_position, index);
3782 }
3783 };
3784
3785 pub fn writeBase(io: std.Io, dir: std.Io.Dir, path: []const u8, source: *const pager.Pager) Error!void {
3786 const phase = trace.scope("file.write_base");
3787 defer phase.end();
3788
3789 var file = try dir.createFile(io, path, .{ .read = true, .truncate = true });
3790 defer file.close(io);
3791 try writeBaseFile(io, file, source);
3792 trace.progress("file.write_base.complete");
3793 }
3794
3795 fn writeBaseFile(io: std.Io, file: std.Io.File, source: *const pager.Pager) Error!void {
3796 var max_page_id: u32 = 0;
3797 for (source.base.items) |image| {
3798 if (image.id == 0) return error.InvalidPageId;
3799 const offset = pageOffset(image.id);
3800 try file.writePositionalAll(io, image.bytes[0..], offset);
3801 max_page_id = @max(max_page_id, image.id);
3802 }
3803
3804 try file.setLength(io, @as(u64, max_page_id) * page.size);
3805 try file.sync(io);
3806 }
3807
3808 fn writeCheckpointPages(
3809 io: std.Io,
3810 file: std.Io.File,
3811 prepared: pager.PreparedCheckpoint,
3812 database_page_count: u32,
3813 ) Error!void {
3814 var max_page_id = database_page_count;
3815 var previous_page_id: u32 = 0;
3816 for (0..prepared.checkpointPageCount()) |page_index| {
3817 const checkpoint_page = prepared.checkpointPage(page_index);
3818 if (checkpoint_page.page_id == 0) return error.InvalidPageId;
3819 std.debug.assert(checkpoint_page.page_id > previous_page_id);
3820 try file.writePositionalAll(io, checkpoint_page.bytes, pageOffset(checkpoint_page.page_id));
3821 max_page_id = @max(max_page_id, checkpoint_page.page_id);
3822 previous_page_id = checkpoint_page.page_id;
3823 }
3824 try file.setLength(io, @as(u64, max_page_id) * page.size);
3825 try file.sync(io);
3826 }
3827
3828 pub fn writeWal(io: std.Io, dir: std.Io.Dir, path: []const u8, source: *const pager.Pager) Error!void {
3829 const phase = trace.scope("file.write_wal");
3830 defer phase.end();
3831
3832 var sidecar_buffer: [wal_sidecar_max]u8 = undefined;
3833 const sidecar = try walSidecarPath(&sidecar_buffer, path);
3834 {
3835 var file = try dir.createFile(io, sidecar, .{ .read = true, .truncate = true });
3836 defer file.close(io);
3837 try writeWalFile(io, file, source);
3838 }
3839 try dir.rename(sidecar, dir, path, io);
3840 trace.progress("file.write_wal.complete");
3841 }
3842
3843 const wal_sidecar_max: usize = artifact_path_bytes_max;
3844 const wal_sidecar_suffix = ".next";
3845
3846 fn walSidecarPath(buffer: *[wal_sidecar_max]u8, path: []const u8) Error![]const u8 {
3847 if (path.len + wal_sidecar_suffix.len > buffer.len) return error.NameTooLong;
3848 @memcpy(buffer[0..path.len], path);
3849 @memcpy(buffer[path.len..][0..wal_sidecar_suffix.len], wal_sidecar_suffix);
3850 return buffer[0 .. path.len + wal_sidecar_suffix.len];
3851 }
3852
3853 fn removeWalSidecar(io: std.Io, dir: std.Io.Dir, path: []const u8) Error!void {
3854 var sidecar_buffer: [wal_sidecar_max]u8 = undefined;
3855 const sidecar = try walSidecarPath(&sidecar_buffer, path);
3856 dir.deleteFile(io, sidecar) catch |err| switch (err) {
3857 error.FileNotFound => {},
3858 else => return err,
3859 };
3860 }
3861
3862 fn writeWalFile(io: std.Io, file: std.Io.File, source: *const pager.Pager) Error!void {
3863 const bytes = source.walBytes();
3864 try file.writePositionalAll(io, bytes, 0);
3865 try file.setLength(io, bytes.len);
3866 try file.sync(io);
3867 }
3868
3869 pub fn recover(
3870 allocator: Allocator,
3871 workspace: *DatabaseWorkspace,
3872 dir: std.Io.Dir,
3873 options: RecoverOptions,
3874 ) Error!pager.Pager {
3875 var state = try recoverState(allocator, workspace, dir, options);
3876 errdefer state.deinit();
3877 if (!state.base_loaded) {
3878 try loadBasePages(
3879 options.io,
3880 &state.pager,
3881 dir,
3882 options.paths.database,
3883 options.control,
3884 );
3885 }
3886 const recovered = state.pager;
3887 state = undefined;
3888 return recovered;
3889 }
3890
3891 fn recoverState(
3892 allocator: Allocator,
3893 workspace: *DatabaseWorkspace,
3894 dir: std.Io.Dir,
3895 options: RecoverOptions,
3896 ) Error!RecoveryState {
3897 const phase = trace.scope("file.recover");
3898 defer phase.end();
3899 const wal_capacity_bytes = options.wal_capacity_bytes orelse options.max_wal_bytes;
3900 if (wal_capacity_bytes > options.max_wal_bytes) return error.InvalidWalLimit;
3901 try options.control.check();
3902
3903 var recovered = try workspace.acquire(allocator, .{
3904 .header = options.header,
3905 .wal_frames = try walFrameCapacity(wal_capacity_bytes),
3906 });
3907 errdefer workspace.release(&recovered);
3908 try removeWalSidecar(options.io, dir, options.paths.wal);
3909 try options.control.check();
3910 const base = try loadBaseLength(options.io, dir, options.paths.database);
3911 recovered.setBasePageCount(base.count);
3912 try options.control.check();
3913 const wal_recovery = try replayWal(
3914 options.io,
3915 allocator,
3916 &recovered,
3917 dir,
3918 options.paths.database,
3919 options.paths.wal,
3920 options.max_wal_bytes,
3921 options.control,
3922 );
3923 try options.control.check();
3924 if (base.torn_tail) {
3925 const torn_page_id = base.count + 1;
3926 const view = try recovered.currentView();
3927 if (try recovered.pageAt(torn_page_id, view) == null) return error.InvalidDatabaseFile;
3928 }
3929 trace.progress("file.recover.complete");
3930 return .{
3931 .workspace = workspace,
3932 .pager = recovered,
3933 .rewrite_base = wal_recovery.rewrite_base,
3934 .rewrite_wal = wal_recovery.rewrite_wal,
3935 .base_loaded = base.count == 0 or wal_recovery.base_loaded,
3936 };
3937 }
3938
3939 const ReadRecoveryResult = union(enum) {
3940 ready: pager.Pager,
3941 repair_required: ReadRepairReason,
3942 };
3943
3944 fn recoverReadOnly(
3945 allocator: Allocator,
3946 workspace: *DatabaseWorkspace,
3947 dir: std.Io.Dir,
3948 options: ReadOpenOptions,
3949 ) Error!ReadRecoveryResult {
3950 try options.control.check();
3951 const base_file = dir.openFile(options.io, options.paths.database, .{}) catch |err| switch (err) {
3952 error.FileNotFound => null,
3953 else => return err,
3954 };
3955 defer if (base_file) |file| file.close(options.io);
3956 const wal_file = dir.openFile(options.io, options.paths.wal, .{}) catch |err| switch (err) {
3957 error.FileNotFound => null,
3958 else => return err,
3959 };
3960 defer if (wal_file) |file| file.close(options.io);
3961 try options.control.check();
3962 return recoverReadOnlyFiles(allocator, workspace, base_file, wal_file, options);
3963 }
3964
3965 fn recoverReadOnlyFiles(
3966 allocator: Allocator,
3967 workspace: *DatabaseWorkspace,
3968 base_file: ?std.Io.File,
3969 wal_file: ?std.Io.File,
3970 options: ReadOpenOptions,
3971 ) Error!ReadRecoveryResult {
3972 try options.control.check();
3973 const base = loadBaseLengthOpen(options.io, base_file) catch |err| switch (err) {
3974 error.InvalidDatabaseFile => return .{ .repair_required = .invalid_database },
3975 else => return err,
3976 };
3977 try options.control.check();
3978 const wal_bytes = if (wal_file) |file|
3979 readOpenFileAlloc(
3980 options.io,
3981 file,
3982 allocator,
3983 options.max_wal_bytes,
3984 options.control,
3985 ) catch |err| switch (err) {
3986 error.StreamTooLong => return .{ .repair_required = .wal_too_large },
3987 else => return err,
3988 }
3989 else
3990 null;
3991 defer if (wal_bytes) |bytes| allocator.free(bytes);
3992 if (!base.exists and wal_bytes == null) return .{ .repair_required = .missing_snapshot };
3993
3994 const mark = if (wal_bytes) |bytes|
3995 wal.endMarkControlled(bytes, options.control) catch |err| switch (err) {
3996 error.Interrupted => return error.Interrupted,
3997 else => return .{ .repair_required = .invalid_wal },
3998 }
3999 else
4000 0;
4001 var recovered = try workspace.acquire(allocator, .{
4002 .header = options.header,
4003 .wal_frames = mark,
4004 });
4005 errdefer workspace.release(&recovered);
4006 recovered.setBasePageCount(base.count);
4007
4008 if (wal_bytes) |bytes| {
4009 try replayReadOnlyWal(allocator, &recovered, base_file, bytes, mark, options);
4010 }
4011
4012 try options.control.check();
4013 const view = try recovered.currentView();
4014 if (base.torn_tail) {
4015 try options.control.check();
4016 if (base.count == std.math.maxInt(u32) or
4017 (try recovered.pageAt(base.count + 1, view)) == null)
4018 {
4019 workspace.release(&recovered);
4020 return .{ .repair_required = .invalid_database };
4021 }
4022 }
4023 var page_number: u64 = if (base.exists) @as(u64, base.count) + 1 else 1;
4024 while (page_number <= recovered.databasePageCount()) : (page_number += 1) {
4025 try options.control.check();
4026 if (try recovered.pageAt(@intCast(page_number), view) != null) continue;
4027 workspace.release(&recovered);
4028 return .{ .repair_required = .invalid_database };
4029 }
4030 try options.control.check();
4031 return .{ .ready = recovered };
4032 }
4033
4034 fn replayReadOnlyWal(
4035 allocator: Allocator,
4036 recovered: *pager.Pager,
4037 base_file: ?std.Io.File,
4038 bytes: []const u8,
4039 mark: usize,
4040 options: ReadOpenOptions,
4041 ) Error!void {
4042 if (mark == 0) return;
4043 const header_matches = std.mem.eql(
4044 u8,
4045 bytes[0..wal.header_size],
4046 recovered.walBytes()[0..wal.header_size],
4047 );
4048 if (header_matches) {
4049 return recovered.replaceWalControlled(
4050 bytes,
4051 wal.header_size + mark * wal.frame_size,
4052 options.control,
4053 );
4054 }
4055 try loadBasePagesOpen(options.io, recovered, base_file, options.control);
4056 var images: std.ArrayList(RecoveryImage) = .empty;
4057 defer images.deinit(allocator);
4058 var reader = try wal.Reader.initControlled(bytes, options.control);
4059 var frame_index: usize = 0;
4060 while (frame_index < mark) : (frame_index += 1) {
4061 const frame = (try reader.nextControlled(options.control)) orelse break;
4062 try upsertRecoveryImage(allocator, &images, frame, options.control);
4063 }
4064 try recovered.reserve(.{ .base_pages = images.items.len });
4065 for (images.items) |image| {
4066 try options.control.check();
4067 try recovered.installBase(image.page_id, &image.bytes);
4068 }
4069 }
4070
4071 const BaseLength = struct {
4072 exists: bool,
4073 count: u32,
4074 torn_tail: bool,
4075 };
4076
4077 fn filePresent(io: std.Io, dir: std.Io.Dir, path: []const u8) Error!bool {
4078 _ = dir.statFile(io, path, .{}) catch |err| switch (err) {
4079 error.FileNotFound => return false,
4080 else => return err,
4081 };
4082 return true;
4083 }
4084
4085 fn loadBaseLength(io: std.Io, dir: std.Io.Dir, path: []const u8) Error!BaseLength {
4086 var file = dir.openFile(io, path, .{}) catch |err| switch (err) {
4087 error.FileNotFound => return .{ .exists = false, .count = 0, .torn_tail = false },
4088 else => return err,
4089 };
4090 defer file.close(io);
4091
4092 return loadBaseLengthOpen(io, file);
4093 }
4094
4095 fn loadBaseLengthOpen(io: std.Io, file: ?std.Io.File) Error!BaseLength {
4096 const opened = file orelse
4097 return .{ .exists = false, .count = 0, .torn_tail = false };
4098
4099 const length = try opened.length(io);
4100 const count = length / page.size;
4101 if (count > std.math.maxInt(u32)) return error.InvalidDatabaseFile;
4102 return .{
4103 .exists = true,
4104 .count = @intCast(count),
4105 .torn_tail = length % page.size != 0,
4106 };
4107 }
4108
4109 fn loadBasePages(
4110 io: std.Io,
4111 target: *pager.Pager,
4112 dir: std.Io.Dir,
4113 path: []const u8,
4114 control: wal.Control,
4115 ) Error!void {
4116 try control.check();
4117 var file = dir.openFile(io, path, .{}) catch |err| switch (err) {
4118 error.FileNotFound => return,
4119 else => return err,
4120 };
4121 defer file.close(io);
4122
4123 try loadBasePagesOpen(io, target, file, control);
4124 }
4125
4126 fn loadBasePagesOpen(
4127 io: std.Io,
4128 target: *pager.Pager,
4129 file: ?std.Io.File,
4130 control: wal.Control,
4131 ) Error!void {
4132 const opened = file orelse return;
4133
4134 try control.check();
4135 const page_count = try loadBasePageCountFromFile(io, opened);
4136 try target.reserve(.{ .base_pages = page_count });
4137
4138 var index: u32 = 0;
4139 while (index < page_count) : (index += 1) {
4140 try control.check();
4141 var image: [page.size]u8 = undefined;
4142 const n = try opened.readPositionalAll(io, image[0..], @as(u64, index) * page.size);
4143 if (n != page.size) return error.InvalidDatabaseFile;
4144 try control.check();
4145 try target.installBase(index + 1, &image);
4146 }
4147 try control.check();
4148 }
4149
4150 fn readOpenFileAlloc(
4151 io: std.Io,
4152 file: std.Io.File,
4153 allocator: Allocator,
4154 max_bytes: usize,
4155 control: wal.Control,
4156 ) Error![]u8 {
4157 try control.check();
4158 const length = try file.length(io);
4159 const count = std.math.cast(usize, length) orelse return error.StreamTooLong;
4160 if (count > max_bytes) return error.StreamTooLong;
4161 try control.check();
4162 const bytes = try allocator.alloc(u8, count);
4163 errdefer allocator.free(bytes);
4164 var filled: usize = 0;
4165 var chunks: usize = 0;
4166 const chunks_max = std.math.divCeil(
4167 usize,
4168 count,
4169 controlled_read_chunk_bytes,
4170 ) catch unreachable;
4171 while (filled < count) : (chunks += 1) {
4172 std.debug.assert(chunks < chunks_max);
4173 try control.check();
4174 const end = filled + @min(controlled_read_chunk_bytes, count - filled);
4175 if (try file.readPositionalAll(io, bytes[filled..end], filled) != end - filled) {
4176 return error.InvalidDatabaseFile;
4177 }
4178 filled = end;
4179 }
4180 try control.check();
4181 return bytes;
4182 }
4183
4184 fn loadBasePageCountFromFile(io: std.Io, file: std.Io.File) Error!u32 {
4185 const length = try file.length(io);
4186 const count = length / page.size;
4187 if (count > std.math.maxInt(u32)) return error.InvalidDatabaseFile;
4188 return @intCast(count);
4189 }
4190
4191 fn replayWal(
4192 io: std.Io,
4193 allocator: Allocator,
4194 target: *pager.Pager,
4195 dir: std.Io.Dir,
4196 database_path: []const u8,
4197 wal_path: []const u8,
4198 max_bytes: usize,
4199 control: wal.Control,
4200 ) Error!WalRecovery {
4201 try control.check();
4202 const bytes = dir.readFileAlloc(io, wal_path, allocator, .limited(max_bytes)) catch |err| switch (err) {
4203 error.FileNotFound => return .{ .rewrite_wal = true },
4204 else => return err,
4205 };
4206 defer allocator.free(bytes);
4207 try control.check();
4208
4209 const header_matches = bytes.len >= wal.header_size and std.mem.eql(u8, bytes[0..wal.header_size], target.walBytes()[0..wal.header_size]);
4210 const rewrite_wal = !std.mem.eql(u8, bytes, target.walBytes());
4211 const mark = try wal.endMarkControlled(bytes, control);
4212 if (mark == 0) return .{ .rewrite_wal = rewrite_wal };
4213
4214 if (header_matches) {
4215 const committed_len = wal.header_size + mark * wal.frame_size;
4216 try target.replaceWalControlled(bytes, committed_len, control);
4217 return .{ .committed_frames = mark, .rewrite_wal = rewrite_wal or committed_len != bytes.len };
4218 }
4219
4220 try loadBasePages(io, target, dir, database_path, control);
4221 try control.check();
4222 var images: std.ArrayList(RecoveryImage) = .empty;
4223 defer images.deinit(allocator);
4224
4225 var reader = try wal.Reader.initControlled(bytes, control);
4226 var frame_index: usize = 0;
4227 while (frame_index < mark) : (frame_index += 1) {
4228 const frame = (try reader.nextControlled(control)) orelse break;
4229 try upsertRecoveryImage(allocator, &images, frame, control);
4230 }
4231
4232 try target.reserve(.{ .base_pages = images.items.len });
4233 for (images.items) |image| {
4234 try control.check();
4235 try target.installBase(image.page_id, &image.bytes);
4236 }
4237 return .{ .committed_frames = mark, .rewrite_base = true, .rewrite_wal = true, .base_loaded = true };
4238 }
4239
4240 fn walFrameCapacity(max_bytes: usize) error{InvalidWalLimit}!usize {
4241 if (max_bytes < wal.header_size) return error.InvalidWalLimit;
4242 return (max_bytes - wal.header_size) / wal.frame_size;
4243 }
4244
4245 fn upsertRecoveryImage(
4246 allocator: Allocator,
4247 images: *std.ArrayList(RecoveryImage),
4248 frame: wal.Frame,
4249 control: wal.Control,
4250 ) (Allocator.Error || error{Interrupted})!void {
4251 for (images.items) |*image| {
4252 try control.check();
4253 if (image.page_id == frame.page_id) {
4254 image.frame = frame.index;
4255 image.bytes = frame.image[0..page.size].*;
4256 return;
4257 }
4258 }
4259 try control.check();
4260 try images.append(allocator, .{
4261 .page_id = frame.page_id,
4262 .frame = frame.index,
4263 .bytes = frame.image[0..page.size].*,
4264 });
4265 }
4266
4267 fn pageOffset(page_id: u32) u64 {
4268 return (@as(u64, page_id) - 1) * page.size;
4269 }
4270
4271 fn testingHeader() wal.Header {
4272 return .{
4273 .sequence = 77,
4274 .salt = .{ .first = 0x1020_3040, .second = 0x5060_7080 },
4275 };
4276 }
4277
4278 fn recoveredHeader() wal.Header {
4279 return .{
4280 .sequence = 78,
4281 .salt = .{ .first = 0x90a0_b0c0, .second = 0xd0e0_f001 },
4282 };
4283 }
4284
4285 const testing_io = std.Options.debug_io;
4286
4287 fn fillImage(image: *[page.size]u8, page_id: u32, value: u8) void {
4288 @memset(image, 0);
4289 image[0] = @intCast(page_id);
4290 image[1] = value;
4291 }
4292
4293 fn testingPager(wal_frames: usize) !pager.Pager {
4294 const options: pager.InitOptions = .{
4295 .header = testingHeader(),
4296 .wal_frames = wal_frames,
4297 };
4298 var workspace = try pager.Pager.Workspace.allocate(std.testing.allocator, options);
4299 return pager.Pager.init(std.testing.allocator, &workspace, options) catch |err| {
4300 workspace.deallocate(std.testing.allocator);
4301 return err;
4302 };
4303 }
4304
4305 fn deinitTestingPager(owned: *pager.Pager) void {
4306 var workspace = owned.deinit();
4307 workspace.deallocate(std.testing.allocator);
4308 }
4309
4310 test "file database workspace reuses exact pager path cache memo and staging regions" {
4311 const limits: DatabaseWorkspace.Limits = .{
4312 .header = testingHeader(),
4313 .max_wal_bytes = wal.header_size + 2 * wal.frame_size,
4314 .path_storage = .{
4315 .database_bytes = 4,
4316 .wal_bytes = 5,
4317 .publication_database_bytes = 6,
4318 .publication_wal_bytes = 7,
4319 },
4320 .read_cache_pages = 2,
4321 };
4322 var workspace = try DatabaseWorkspace.allocate(std.testing.allocator, limits);
4323 defer workspace.deallocate(std.testing.allocator);
4324 const journal_pointer = workspace.storage.pager.journal.ptr;
4325 const checkpoint_pointer = workspace.storage.pager.checkpoint.ptr;
4326 const checkpoint_once_pointer = workspace.storage.pager.checkpoint_once.ptr;
4327 const wal_index_pointer = workspace.storage.pager.wal_index.ptr;
4328 const path_storage_pointer = workspace.storage.paths.ptr;
4329 const read_cache_pointer = workspace.storage.read_cache.ptr;
4330 const digest_memo_pointer = workspace.storage.digest_memo.ptr;
4331 const transaction_staging_pointer = workspace.storage.transaction_staging.ptr;
4332
4333 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 });
4334 var first = try workspace.acquire(failing.allocator(), .{
4335 .header = testingHeader(),
4336 .wal_frames = 1,
4337 });
4338 try std.testing.expectError(error.WorkspaceBusy, workspace.acquire(failing.allocator(), .{
4339 .header = testingHeader(),
4340 .wal_frames = 1,
4341 }));
4342 workspace.release(&first);
4343 try std.testing.expect(!failing.has_induced_failure);
4344
4345 var second = try workspace.acquire(failing.allocator(), .{
4346 .header = recoveredHeader(),
4347 .wal_frames = 2,
4348 });
4349 workspace.release(&second);
4350 try std.testing.expectEqual(journal_pointer, workspace.storage.pager.journal.ptr);
4351 try std.testing.expectEqual(checkpoint_pointer, workspace.storage.pager.checkpoint.ptr);
4352 try std.testing.expectEqual(checkpoint_once_pointer, workspace.storage.pager.checkpoint_once.ptr);
4353 try std.testing.expectEqual(wal_index_pointer, workspace.storage.pager.wal_index.ptr);
4354 try std.testing.expectError(error.WorkspaceCapacityExceeded, workspace.acquire(failing.allocator(), .{
4355 .header = testingHeader(),
4356 .wal_frames = 3,
4357 }));
4358 try std.testing.expectEqual(DatabaseWorkspace.State.free, workspace.state);
4359
4360 const path_limits: PathStorage.Limits = .{
4361 .database_bytes = 4,
4362 .wal_bytes = 5,
4363 .publication_database_bytes = 6,
4364 .publication_wal_bytes = 7,
4365 };
4366 var path_storage = try workspace.acquirePathStorage(path_limits);
4367 try std.testing.expectError(
4368 error.WorkspaceBusy,
4369 workspace.acquirePathStorage(path_limits),
4370 );
4371 _ = try path_storage.storeCurrent(.{ .database = "main", .wal = "write" });
4372 _ = try path_storage.storePublication(.{ .database = "origin", .wal = "journal" });
4373 try workspace.releasePathStorage(&path_storage);
4374 try std.testing.expectEqual(path_storage_pointer, workspace.storage.paths.ptr);
4375 var overcapacity_path_limits = path_limits;
4376 overcapacity_path_limits.database_bytes += 1;
4377 try std.testing.expectError(
4378 error.WorkspaceCapacityExceeded,
4379 workspace.acquirePathStorage(overcapacity_path_limits),
4380 );
4381 var reshaped_path_limits = path_limits;
4382 reshaped_path_limits.database_bytes += 1;
4383 reshaped_path_limits.wal_bytes -= 1;
4384 try std.testing.expectError(
4385 error.WorkspaceCapacityExceeded,
4386 workspace.acquirePathStorage(reshaped_path_limits),
4387 );
4388
4389 var read_cache = try workspace.acquireReadCache(.{ .pages = 2 });
4390 read_cache.activate();
4391 try std.testing.expectError(
4392 error.WorkspaceBusy,
4393 workspace.acquireReadCache(.{ .pages = 1 }),
4394 );
4395 workspace.releaseReadCache(&read_cache);
4396 try std.testing.expectEqual(read_cache_pointer, workspace.storage.read_cache.ptr);
4397 try std.testing.expectError(
4398 error.WorkspaceCapacityExceeded,
4399 workspace.acquireReadCache(.{ .pages = 3 }),
4400 );
4401
4402 var digest_memo = try workspace.acquireDigestMemo();
4403 try std.testing.expectEqual(limits.digest_memo_entries, digest_memo.slots.len);
4404 try std.testing.expectError(error.WorkspaceBusy, workspace.acquireDigestMemo());
4405 workspace.releaseDigestMemo(&digest_memo);
4406 try std.testing.expectEqual(digest_memo_pointer, workspace.storage.digest_memo.ptr);
4407
4408 var staging = try workspace.acquireTransactionStaging(.{ .frames = 2 });
4409 staging.activate();
4410 try std.testing.expectError(
4411 error.WorkspaceBusy,
4412 workspace.acquireTransactionStaging(.{ .frames = 1 }),
4413 );
4414 workspace.releaseTransactionStaging(&staging);
4415 try std.testing.expectEqual(
4416 transaction_staging_pointer,
4417 workspace.storage.transaction_staging.ptr,
4418 );
4419 try std.testing.expectError(
4420 error.WorkspaceCapacityExceeded,
4421 workspace.acquireTransactionStaging(.{ .frames = 3 }),
4422 );
4423 }
4424
4425 test "file database workspace rejects short maximum storage and returns full regions" {
4426 const limits: DatabaseWorkspace.Limits = .{
4427 .header = testingHeader(),
4428 .max_wal_bytes = wal.header_size + 2 * wal.frame_size,
4429 .path_storage = .{ .database_bytes = 3, .wal_bytes = 4 },
4430 };
4431 const capacity = try DatabaseWorkspace.Capacity.derive(limits);
4432 var pager_storage = try pager.Pager.Workspace.allocate(std.testing.allocator, .{
4433 .header = limits.header,
4434 .wal_frames = capacity.wal_frames,
4435 });
4436 defer pager_storage.deallocate(std.testing.allocator);
4437 const transaction_staging = try std.testing.allocator.alignedAlloc(
4438 u8,
4439 .fromByteUnits(TransactionStaging.storage_alignment),
4440 capacity.transaction_staging.storage_bytes,
4441 );
4442 defer std.testing.allocator.free(transaction_staging);
4443 const read_cache = try std.testing.allocator.alignedAlloc(
4444 u8,
4445 .fromByteUnits(ReadCache.storage_alignment),
4446 capacity.read_cache.storage_bytes,
4447 );
4448 defer std.testing.allocator.free(read_cache);
4449 const digest_memo = try std.testing.allocator.alloc(
4450 DigestMemo.Entry,
4451 capacity.digest_memo.entries,
4452 );
4453 defer std.testing.allocator.free(digest_memo);
4454 const paths = try std.testing.allocator.alloc(u8, capacity.paths.storage_bytes);
4455 defer std.testing.allocator.free(paths);
4456 var storage = DatabaseWorkspace.Storage{
4457 .pager = pager_storage,
4458 .paths = paths,
4459 .read_cache = read_cache,
4460 .digest_memo = digest_memo,
4461 .transaction_staging = transaction_staging,
4462 };
4463 const complete_checkpoint = storage.pager.checkpoint;
4464 storage.pager.checkpoint = storage.pager.checkpoint[0 .. storage.pager.checkpoint.len - 1];
4465 try std.testing.expectError(error.StorageTooShort, DatabaseWorkspace.init(storage, limits));
4466 storage.pager.checkpoint = complete_checkpoint;
4467 const complete_transaction_staging = storage.transaction_staging;
4468 storage.transaction_staging = storage.transaction_staging[0 .. storage.transaction_staging.len - 1];
4469 try std.testing.expectError(error.StorageTooShort, DatabaseWorkspace.init(storage, limits));
4470 storage.transaction_staging = complete_transaction_staging;
4471 const complete_read_cache = storage.read_cache;
4472 storage.read_cache = storage.read_cache[0 .. storage.read_cache.len - 1];
4473 try std.testing.expectError(error.StorageTooShort, DatabaseWorkspace.init(storage, limits));
4474 storage.read_cache = complete_read_cache;
4475 const complete_digest_memo = storage.digest_memo;
4476 storage.digest_memo = storage.digest_memo[0 .. storage.digest_memo.len - 1];
4477 try std.testing.expectError(error.StorageTooShort, DatabaseWorkspace.init(storage, limits));
4478 storage.digest_memo = complete_digest_memo;
4479 const complete_paths = storage.paths;
4480 storage.paths = storage.paths[0 .. storage.paths.len - 1];
4481 try std.testing.expectError(error.StorageTooShort, DatabaseWorkspace.init(storage, limits));
4482 storage.paths = complete_paths;
4483
4484 var workspace = try DatabaseWorkspace.init(storage, limits);
4485 workspace.activate();
4486 const returned = workspace.deinit();
4487 try std.testing.expectEqual(storage.pager.journal.ptr, returned.pager.journal.ptr);
4488 try std.testing.expectEqual(storage.pager.journal.len, returned.pager.journal.len);
4489 try std.testing.expectEqual(storage.pager.checkpoint.ptr, returned.pager.checkpoint.ptr);
4490 try std.testing.expectEqual(storage.pager.checkpoint.len, returned.pager.checkpoint.len);
4491 try std.testing.expectEqual(storage.pager.checkpoint_once.ptr, returned.pager.checkpoint_once.ptr);
4492 try std.testing.expectEqual(storage.pager.checkpoint_once.len, returned.pager.checkpoint_once.len);
4493 try std.testing.expectEqual(storage.pager.wal_index.ptr, returned.pager.wal_index.ptr);
4494 try std.testing.expectEqual(storage.pager.wal_index.len, returned.pager.wal_index.len);
4495 try std.testing.expectEqual(storage.paths.ptr, returned.paths.ptr);
4496 try std.testing.expectEqual(storage.paths.len, returned.paths.len);
4497 try std.testing.expectEqual(storage.read_cache.ptr, returned.read_cache.ptr);
4498 try std.testing.expectEqual(storage.read_cache.len, returned.read_cache.len);
4499 try std.testing.expectEqual(storage.digest_memo.ptr, returned.digest_memo.ptr);
4500 try std.testing.expectEqual(storage.digest_memo.len, returned.digest_memo.len);
4501 try std.testing.expectEqual(storage.transaction_staging.ptr, returned.transaction_staging.ptr);
4502 try std.testing.expectEqual(storage.transaction_staging.len, returned.transaction_staging.len);
4503 storage = returned;
4504 }
4505
4506 test "file database rejects path overcapacity before opening files" {
4507 var tmp = std.testing.tmpDir(.{});
4508 defer tmp.cleanup();
4509 var workspace = try DatabaseWorkspace.allocate(std.testing.allocator, .{
4510 .header = testingHeader(),
4511 .max_wal_bytes = default_max_wal_bytes,
4512 .path_storage = .{ .database_bytes = 1, .wal_bytes = 1 },
4513 });
4514 defer workspace.deallocate(std.testing.allocator);
4515
4516 try std.testing.expectError(
4517 error.WorkspaceCapacityExceeded,
4518 Database.open(std.testing.allocator, &workspace, tmp.dir, .{
4519 .paths = .{ .database = "capacity.db", .wal = "capacity.wal" },
4520 .header = testingHeader(),
4521 }),
4522 );
4523 try std.testing.expect(!workspace.path_storage.loan_live);
4524 try std.testing.expect(!workspace.read_cache_live);
4525 try std.testing.expectError(
4526 error.FileNotFound,
4527 tmp.dir.openFile(testing_io, "capacity.db", .{}),
4528 );
4529 try std.testing.expectError(
4530 error.FileNotFound,
4531 tmp.dir.openFile(testing_io, "capacity.wal", .{}),
4532 );
4533 }
4534
4535 test "file database rejects read cache overcapacity before opening files" {
4536 var tmp = std.testing.tmpDir(.{});
4537 defer tmp.cleanup();
4538 var workspace = try DatabaseWorkspace.allocate(std.testing.allocator, .{
4539 .header = testingHeader(),
4540 .max_wal_bytes = default_max_wal_bytes,
4541 .path_storage = PathStorage.Limits.forDirect(.{
4542 .database = "capacity.db",
4543 .wal = "capacity.wal",
4544 }),
4545 .read_cache_pages = 1,
4546 });
4547 defer workspace.deallocate(std.testing.allocator);
4548
4549 try std.testing.expectError(
4550 error.WorkspaceCapacityExceeded,
4551 Database.open(std.testing.allocator, &workspace, tmp.dir, .{
4552 .paths = .{ .database = "capacity.db", .wal = "capacity.wal" },
4553 .header = testingHeader(),
4554 .read_cache_capacity = 2,
4555 }),
4556 );
4557 try std.testing.expect(!workspace.read_cache_live);
4558 try std.testing.expectEqual(DatabaseWorkspace.State.free, workspace.state);
4559 try std.testing.expectError(
4560 error.FileNotFound,
4561 tmp.dir.openFile(testing_io, "capacity.db", .{}),
4562 );
4563 try std.testing.expectError(
4564 error.FileNotFound,
4565 tmp.dir.openFile(testing_io, "capacity.wal", .{}),
4566 );
4567 }
4568
4569 fn testSnapshotPageByte(snapshot: Snapshot, page_id: u32) Error!?u8 {
4570 var image: [page.size]u8 = undefined;
4571 if (!(try snapshot.copyPage(page_id, &image))) return null;
4572 return image[1];
4573 }
4574
4575 fn testDatabasePageByte(database: *Database, page_id: u32) Error!?u8 {
4576 var read = try database.beginRead();
4577 defer read.deinit();
4578 return try testSnapshotPageByte(read.snapshot(), page_id);
4579 }
4580
4581 fn openReadOnlyForTest(dir: std.Io.Dir, options: ReadOpenOptions) !ReadOnlyDatabase {
4582 const opened = try ReadOnlyDatabase.openForTesting(std.testing.allocator, dir, options);
4583 return switch (opened) {
4584 .ready => |ready| ready,
4585 .repair_required => error.UnexpectedRepairRequired,
4586 };
4587 }
4588
4589 fn openReadOnlyWithWorkspaceForTest(
4590 workspace: *ReadOnlyDatabase.Workspace,
4591 dir: std.Io.Dir,
4592 options: ReadOpenOptions,
4593 ) !ReadOnlyDatabase {
4594 return switch (try ReadOnlyDatabase.openExisting(
4595 std.testing.allocator,
4596 workspace,
4597 dir,
4598 options,
4599 )) {
4600 .ready => |ready| ready,
4601 .repair_required => error.UnexpectedRepairRequired,
4602 };
4603 }
4604
4605 fn expectReadRepair(dir: std.Io.Dir, options: ReadOpenOptions, expected: ReadRepairReason) !void {
4606 const opened = try ReadOnlyDatabase.openForTesting(std.testing.allocator, dir, options);
4607 switch (opened) {
4608 .repair_required => |reason| try std.testing.expectEqual(expected, reason),
4609 .ready => |ready| {
4610 var database = ready;
4611 database.deinit();
4612 return error.ExpectedRepairRequired;
4613 },
4614 }
4615 }
4616
4617 fn openSparseReadOnlyForTest(
4618 dir: std.Io.Dir,
4619 paths: Paths,
4620 scratch: *[wal.frame_size]u8,
4621 refs: []wal.FrameRef,
4622 probe: ?*SparseReadProbe,
4623 ) !SparseReadOnlyDatabase {
4624 const opened = try SparseReadOnlyDatabase.openExisting(dir, .{
4625 .paths = paths,
4626 .workspace = .{ .scratch = scratch, .refs = refs },
4627 .probe = probe,
4628 });
4629 return switch (opened) {
4630 .ready => |ready| ready,
4631 .repair_required => error.UnexpectedRepairRequired,
4632 };
4633 }
4634
4635 fn expectSparseReadRepair(
4636 dir: std.Io.Dir,
4637 paths: Paths,
4638 scratch: *[wal.frame_size]u8,
4639 refs: []wal.FrameRef,
4640 probe: ?*SparseReadProbe,
4641 expected: ReadRepairReason,
4642 ) !void {
4643 const opened = try SparseReadOnlyDatabase.openExisting(dir, .{
4644 .paths = paths,
4645 .workspace = .{ .scratch = scratch, .refs = refs },
4646 .probe = probe,
4647 });
4648 switch (opened) {
4649 .repair_required => |reason| try std.testing.expectEqual(expected, reason),
4650 .ready => |ready| {
4651 var database = ready;
4652 database.deinit();
4653 return error.ExpectedRepairRequired;
4654 },
4655 }
4656 }
4657
4658 const TestingInterrupt = struct {
4659 remaining: usize,
4660 enabled: bool = true,
4661 calls: usize = 0,
4662
4663 fn control(self: *TestingInterrupt) wal.Control {
4664 return .{ .context = self, .interrupted_fn = interrupted };
4665 }
4666
4667 fn interrupted(context: ?*anyopaque) bool {
4668 const self: *TestingInterrupt = @ptrCast(@alignCast(context.?));
4669 self.calls += 1;
4670 if (!self.enabled) return false;
4671 if (self.remaining == 0) return true;
4672 self.remaining -= 1;
4673 return false;
4674 }
4675 };
4676
4677 fn commitTestPage(database: *Database, page_id: u32, value: u8) !void {
4678 var image: [page.size]u8 = undefined;
4679 fillImage(&image, page_id, value);
4680 var transaction = try database.beginWrite();
4681 defer transaction.deinit();
4682 try transaction.putPage(page_id, &image);
4683 _ = try transaction.commit(.{});
4684 }
4685
4686 test "file publication alternates immutable lanes across commits" {
4687 const testing = std.testing;
4688 var tmp = testing.tmpDir(.{});
4689 defer tmp.cleanup();
4690 const base = Paths{ .database = "published.db", .wal = "published.wal" };
4691 const first: publication.Token = 1;
4692 const second: publication.Token = 2;
4693 var database = try Database.openForTesting(testing.allocator, tmp.dir, .{
4694 .header = testingHeader(),
4695 .publication = .{
4696 .base_paths = base,
4697 .selected = null,
4698 .candidate = first,
4699 },
4700 });
4701 defer database.deinit();
4702
4703 try commitTestPage(&database, 1, 41);
4704 var oldest_read = try database.beginRead();
4705 defer oldest_read.deinit();
4706 const oldest = oldest_read.snapshot();
4707 try testing.expectEqual(first, try database.preparePublication());
4708 try testing.expectError(error.RecoveryRequired, database.beginWrite());
4709 const first_lane = try publication.Lane.init(.{
4710 .database = base.database,
4711 .wal = base.wal,
4712 }, 0);
4713 try testing.expectEqual(
4714 publication.Inspect.ready,
4715 try publication.inspect(testing_io, tmp.dir, &first_lane, first),
4716 );
4717
4718 database.publicationCommitted(second);
4719 try commitTestPage(&database, 1, 82);
4720 try testing.expectEqual(@as(u8, 41), (try testSnapshotPageByte(oldest, 1)).?);
4721 try testing.expectEqual(second, try database.preparePublication());
4722 const second_lane = try publication.Lane.init(.{
4723 .database = base.database,
4724 .wal = base.wal,
4725 }, 1);
4726 try testing.expectEqual(
4727 publication.Inspect.ready,
4728 try publication.inspect(testing_io, tmp.dir, &first_lane, first),
4729 );
4730 try testing.expectEqual(
4731 publication.Inspect.ready,
4732 try publication.inspect(testing_io, tmp.dir, &second_lane, second),
4733 );
4734 }
4735
4736 test "file publication opens a selected lane through a fresh candidate" {
4737 const testing = std.testing;
4738 var tmp = testing.tmpDir(.{});
4739 defer tmp.cleanup();
4740 const base = Paths{ .database = "reopen.db", .wal = "reopen.wal" };
4741 const first: publication.Token = 1;
4742 const second: publication.Token = 2;
4743 {
4744 var seed = try Database.openForTesting(testing.allocator, tmp.dir, .{
4745 .header = testingHeader(),
4746 .publication = .{ .base_paths = base, .selected = null, .candidate = first },
4747 });
4748 defer seed.deinit();
4749 try commitTestPage(&seed, 1, 61);
4750 try testing.expectEqual(first, try seed.preparePublication());
4751 seed.publicationCommitted(second);
4752 }
4753
4754 var reopened = try Database.openForTesting(testing.allocator, tmp.dir, .{
4755 .header = testingHeader(),
4756 .publication = .{ .base_paths = base, .selected = first, .candidate = second },
4757 });
4758 defer reopened.deinit();
4759 var initial_read = try reopened.beginRead();
4760 defer initial_read.deinit();
4761 try testing.expectEqual(
4762 @as(u8, 61),
4763 (try testSnapshotPageByte(initial_read.snapshot(), 1)).?,
4764 );
4765 const first_lane = try publication.Lane.init(.{
4766 .database = base.database,
4767 .wal = base.wal,
4768 }, 0);
4769 try testing.expectEqual(
4770 publication.Inspect.ready,
4771 try publication.inspect(testing_io, tmp.dir, &first_lane, first),
4772 );
4773 try commitTestPage(&reopened, 1, 62);
4774 try testing.expectEqual(second, try reopened.preparePublication());
4775 }
4776
4777 test "file publication lane path replacement remains allocation free" {
4778 const testing = std.testing;
4779 var tmp = testing.tmpDir(.{});
4780 defer tmp.cleanup();
4781 var failing = testing.FailingAllocator.init(testing.allocator, .{
4782 .fail_index = std.math.maxInt(usize),
4783 });
4784 const base = Paths{ .database = "allocation.db", .wal = "allocation.wal" };
4785 var database = try Database.openForTesting(failing.allocator(), tmp.dir, .{
4786 .header = testingHeader(),
4787 .publication = .{ .base_paths = base, .selected = null, .candidate = 1 },
4788 });
4789 defer database.deinit();
4790 try commitTestPage(&database, 1, 61);
4791 try testing.expectEqual(@as(publication.Token, 1), try database.preparePublication());
4792 database.publicationCommitted(2);
4793 failing.fail_index = failing.alloc_index;
4794 try commitTestPage(&database, 1, 62);
4795 try testing.expect(!failing.has_induced_failure);
4796 try testing.expect(!database.requiresRecovery());
4797 try testing.expectEqual(@as(publication.Token, 2), try database.preparePublication());
4798 }
4799
4800 test "file publication pinned writer read survives lazy base and two lane reuses" {
4801 const testing = std.testing;
4802 var tmp = testing.tmpDir(.{});
4803 defer tmp.cleanup();
4804 const base = Paths{ .database = "reuse.db", .wal = "reuse.wal" };
4805 var database = try Database.openForTesting(testing.allocator, tmp.dir, .{
4806 .header = testingHeader(),
4807 .publication = .{
4808 .base_paths = base,
4809 .selected = null,
4810 .candidate = 1,
4811 },
4812 });
4813 defer database.deinit();
4814
4815 try commitTestPage(&database, 1, 41);
4816 _ = try database.checkpoint(.{ .restart_header = testingHeader() });
4817 try testing.expectEqual(@as(PublicationToken, 1), try database.preparePublication());
4818 var oldest_read = try database.beginRead();
4819 defer oldest_read.deinit();
4820 const oldest = oldest_read.snapshot();
4821
4822 database.publicationCommitted(2);
4823 try commitTestPage(&database, 1, 82);
4824 const second_checkpoint = try database.checkpoint(.{
4825 .restart_header = recoveredHeader(),
4826 });
4827 try testing.expect(!second_checkpoint.restarted);
4828 try testing.expectEqual(@as(PublicationToken, 2), try database.preparePublication());
4829
4830 database.publicationCommitted(3);
4831 try commitTestPage(&database, 1, 123);
4832 const third_checkpoint = try database.checkpoint(.{
4833 .restart_header = testingHeader(),
4834 });
4835 try testing.expect(!third_checkpoint.restarted);
4836 try testing.expectEqual(@as(PublicationToken, 3), try database.preparePublication());
4837
4838 try testing.expectEqual(@as(u8, 41), (try testSnapshotPageByte(oldest, 1)).?);
4839 try testing.expectEqual(
4840 @as(u8, 123),
4841 (try testDatabasePageByte(&database, 1)).?,
4842 );
4843 }
4844
4845 test "file read leases share the base file unless lanes can replace it" {
4846 const testing = std.testing;
4847 var tmp = testing.tmpDir(.{});
4848 defer tmp.cleanup();
4849 const plain_paths = Paths{ .database = "plain.db", .wal = "plain.wal" };
4850 {
4851 var database = try Database.openForTesting(testing.allocator, tmp.dir, .{
4852 .paths = plain_paths,
4853 .header = testingHeader(),
4854 });
4855 defer database.deinit();
4856 try commitTestPage(&database, 1, 17);
4857 _ = try database.checkpoint(.{ .restart_header = testingHeader() });
4858 }
4859
4860 var plain = try Database.openForTesting(testing.allocator, tmp.dir, .{
4861 .paths = plain_paths,
4862 .header = testingHeader(),
4863 });
4864 defer plain.deinit();
4865 var first = try plain.beginRead();
4866 defer first.deinit();
4867 var second = try plain.beginRead();
4868 defer second.deinit();
4869 for ([_]u8{ first.index, second.index }) |lease_index| {
4870 const lease_file = plain.read_leases.slots[lease_index].base_file.?;
4871 try testing.expect(lease_file == .borrowed);
4872 try testing.expect(lease_file.borrowed == &plain.base_file);
4873 }
4874 try testing.expectEqual(@as(u8, 17), (try testSnapshotPageByte(first.snapshot(), 1)).?);
4875 try testing.expectEqual(@as(u8, 17), (try testSnapshotPageByte(second.snapshot(), 1)).?);
4876
4877 var published = try Database.openForTesting(testing.allocator, tmp.dir, .{
4878 .header = testingHeader(),
4879 .publication = .{
4880 .base_paths = .{ .database = "published.db", .wal = "published.wal" },
4881 .selected = null,
4882 .candidate = 1,
4883 },
4884 });
4885 defer published.deinit();
4886 var owned = try published.beginRead();
4887 defer owned.deinit();
4888 try testing.expect(published.read_leases.slots[owned.index].base_file.? == .owned);
4889 }
4890
4891 test "file read leases reject at the configured fixed capacity and reuse released slots" {
4892 const testing = std.testing;
4893 var tmp = testing.tmpDir(.{});
4894 defer tmp.cleanup();
4895 var database = try Database.openForTesting(testing.allocator, tmp.dir, .{
4896 .paths = .{ .database = "capacity.db", .wal = "capacity.wal" },
4897 .header = testingHeader(),
4898 .read_lease_limit = 1,
4899 });
4900 defer database.deinit();
4901
4902 var first = try database.beginRead();
4903 const index = first.index;
4904 const serial = first.serial;
4905 try testing.expectError(
4906 error.ReadLeaseCapacityExceeded,
4907 database.beginRead(),
4908 );
4909 first.deinit();
4910
4911 var reused = try database.beginRead();
4912 defer reused.deinit();
4913 try testing.expectEqual(index, reused.index);
4914 try testing.expect(serial != reused.serial);
4915 }
4916
4917 test "file read lease acquisition failure leaves no live slot" {
4918 const testing = std.testing;
4919 var tmp = testing.tmpDir(.{});
4920 defer tmp.cleanup();
4921 var database = try Database.openForTesting(testing.allocator, tmp.dir, .{
4922 .header = testingHeader(),
4923 .publication = .{
4924 .base_paths = .{ .database = "cleanup.db", .wal = "cleanup.wal" },
4925 .selected = null,
4926 .candidate = 1,
4927 },
4928 .read_lease_limit = 1,
4929 });
4930 defer database.deinit();
4931
4932 const lane_database = database.paths.database;
4933 try tmp.dir.rename(lane_database, tmp.dir, "cleanup.retired", testing_io);
4934 try testing.expectError(error.FileNotFound, database.beginRead());
4935 try testing.expectEqual(@as(u8, 0), database.read_leases.active);
4936 try tmp.dir.rename("cleanup.retired", tmp.dir, lane_database, testing_io);
4937 var read = try database.beginRead();
4938 defer read.deinit();
4939 try testing.expectEqual(@as(u8, 1), database.read_leases.active);
4940 }
4941
4942 test "file publication reports an opaque physical footprint" {
4943 const testing = std.testing;
4944 var tmp = testing.tmpDir(.{});
4945 defer tmp.cleanup();
4946 const base = Paths{ .database = "footprint.db", .wal = "footprint.wal" };
4947 var database = try Database.openForTesting(testing.allocator, tmp.dir, .{
4948 .header = testingHeader(),
4949 .publication = .{ .base_paths = base, .selected = null, .candidate = 1 },
4950 });
4951 defer database.deinit();
4952 try commitTestPage(&database, 1, 73);
4953 _ = try database.checkpoint(.{ .restart_header = testingHeader() });
4954 _ = try database.preparePublication();
4955 const footprint = try publicationFootprint(testing_io, tmp.dir, base);
4956 try testing.expect(footprint.database_bytes >= page.size);
4957 try testing.expect(footprint.wal_bytes >= wal.header_size);
4958 try testing.expect(footprint.metadata_bytes != 0);
4959 try testing.expectEqual(
4960 footprint.database_bytes + footprint.wal_bytes + footprint.metadata_bytes,
4961 footprint.totalBytes(),
4962 );
4963 }
4964
4965 test "file artifact inventory owns canonical and publication paths" {
4966 const testing = std.testing;
4967 const paths = Paths{ .database = "inventory.db", .wal = "inventory.wal" };
4968 var buffer: [artifact_path_bytes_max]u8 = undefined;
4969 try testing.expectEqualStrings("inventory.db", (try artifact(paths, 0, &buffer)).?);
4970 try testing.expectEqualStrings("inventory.wal", (try artifact(paths, 1, &buffer)).?);
4971 try testing.expectEqualStrings("inventory.wal.next", (try artifact(paths, 2, &buffer)).?);
4972 var count: usize = 0;
4973 while (try artifact(paths, count, &buffer)) |_| count += 1;
4974 try testing.expectEqual(@as(usize, 15), count);
4975 }
4976
4977 test "file published reader pins a lazy base across lane reuse" {
4978 const testing = std.testing;
4979 var tmp = testing.tmpDir(.{});
4980 defer tmp.cleanup();
4981 const base = Paths{ .database = "pinned.db", .wal = "pinned.wal" };
4982 {
4983 var seed = try Database.openForTesting(testing.allocator, tmp.dir, .{
4984 .header = testingHeader(),
4985 .publication = .{ .base_paths = base, .selected = null, .candidate = 1 },
4986 });
4987 defer seed.deinit();
4988 try commitTestPage(&seed, 1, 41);
4989 _ = try seed.checkpoint(.{ .restart_header = testingHeader() });
4990 try testing.expectEqual(@as(publication.Token, 1), try seed.preparePublication());
4991 }
4992
4993 var reader = try Database.openPublishedReadOnlyForTesting(testing.allocator, tmp.dir, .{
4994 .base_paths = base,
4995 .token = 1,
4996 .header = testingHeader(),
4997 });
4998 defer reader.deinit();
4999 var pinned_read = try reader.beginRead();
5000 defer pinned_read.deinit();
5001 const pinned = pinned_read.snapshot();
5002 try testing.expectError(error.ReadOnlyDatabase, reader.beginWrite());
5003
5004 var writer = try Database.openForTesting(testing.allocator, tmp.dir, .{
5005 .header = testingHeader(),
5006 .publication = .{ .base_paths = base, .selected = 1, .candidate = 2 },
5007 });
5008 defer writer.deinit();
5009 try commitTestPage(&writer, 1, 82);
5010 _ = try writer.checkpoint(.{ .restart_header = testingHeader() });
5011 try testing.expectEqual(@as(publication.Token, 2), try writer.preparePublication());
5012 writer.publicationCommitted(3);
5013 try commitTestPage(&writer, 1, 123);
5014
5015 var late_read = try reader.beginRead();
5016 defer late_read.deinit();
5017 try testing.expectEqual(
5018 @as(u8, 41),
5019 (try testSnapshotPageByte(late_read.snapshot(), 1)).?,
5020 );
5021 try testing.expectEqual(@as(u8, 41), (try testSnapshotPageByte(pinned, 1)).?);
5022 var current_read = try writer.beginRead();
5023 defer current_read.deinit();
5024 try testing.expectEqual(
5025 @as(u8, 123),
5026 (try testSnapshotPageByte(current_read.snapshot(), 1)).?,
5027 );
5028 }
5029
5030 test "file ordinary reader pins files and rejects mutation" {
5031 const testing = std.testing;
5032 var tmp = testing.tmpDir(.{});
5033 defer tmp.cleanup();
5034 const paths = Paths{ .database = "ordinary.db", .wal = "ordinary.wal" };
5035 {
5036 var writer = try Database.openForTesting(testing.allocator, tmp.dir, .{
5037 .paths = paths,
5038 .header = testingHeader(),
5039 });
5040 defer writer.deinit();
5041 try commitTestPage(&writer, 1, 37);
5042 _ = try writer.checkpoint(.{ .restart_header = testingHeader() });
5043 }
5044
5045 var reader = try Database.openReadOnlyForTesting(testing.allocator, tmp.dir, .{
5046 .paths = paths,
5047 .header = testingHeader(),
5048 });
5049 defer reader.deinit();
5050 try testing.expectError(error.ReadOnlyDatabase, reader.beginWrite());
5051 var read = try reader.beginRead();
5052 defer read.deinit();
5053 const snapshot = read.snapshot();
5054
5055 try tmp.dir.rename(paths.database, tmp.dir, "ordinary-retired.db", testing_io);
5056 try tmp.dir.rename(paths.wal, tmp.dir, "ordinary-retired.wal", testing_io);
5057 try testing.expectEqual(@as(u8, 37), (try testSnapshotPageByte(snapshot, 1)).?);
5058 }
5059
5060 test "file writes base and wal then recovers committed page images" {
5061 var tmp = std.testing.tmpDir(.{});
5062 defer tmp.cleanup();
5063
5064 var source = try testingPager(1);
5065 defer deinitTestingPager(&source);
5066
5067 var base: [page.size]u8 = undefined;
5068 var committed: [page.size]u8 = undefined;
5069 fillImage(&base, 1, 10);
5070 fillImage(&committed, 1, 20);
5071
5072 try source.installBase(1, &base);
5073 try source.appendWal(1, 1, &committed);
5074 try writeBase(testing_io, tmp.dir, "main.db", &source);
5075 try writeWal(testing_io, tmp.dir, "main.wal", &source);
5076
5077 var workspace = try DatabaseWorkspace.allocate(std.testing.allocator, .{
5078 .header = recoveredHeader(),
5079 .max_wal_bytes = default_max_wal_bytes,
5080 .path_storage = .{ .database_bytes = 0, .wal_bytes = 0 },
5081 });
5082 defer workspace.deallocate(std.testing.allocator);
5083 var recovered = try recover(std.testing.allocator, &workspace, tmp.dir, .{
5084 .paths = .{ .database = "main.db", .wal = "main.wal" },
5085 .header = recoveredHeader(),
5086 });
5087 defer workspace.release(&recovered);
5088
5089 const snapshot = try recovered.beginRead();
5090 try std.testing.expectEqual(@as(usize, 0), snapshot.view.end_mark);
5091 try std.testing.expectEqual(@as(usize, 0), recovered.frameCount());
5092 try std.testing.expectEqual(@as(u8, 20), (try snapshot.get(1)).?[1]);
5093 }
5094
5095 test "file recovery ignores uncommitted wal tail" {
5096 var tmp = std.testing.tmpDir(.{});
5097 defer tmp.cleanup();
5098
5099 var source = try testingPager(2);
5100 defer deinitTestingPager(&source);
5101
5102 var base: [page.size]u8 = undefined;
5103 var committed: [page.size]u8 = undefined;
5104 var uncommitted: [page.size]u8 = undefined;
5105 fillImage(&base, 1, 10);
5106 fillImage(&committed, 1, 20);
5107 fillImage(&uncommitted, 1, 99);
5108
5109 try source.installBase(1, &base);
5110 try source.appendWal(1, 1, &committed);
5111 try source.appendWal(1, 0, &uncommitted);
5112 try writeBase(testing_io, tmp.dir, "main.db", &source);
5113 try writeWal(testing_io, tmp.dir, "main.wal", &source);
5114
5115 var workspace = try DatabaseWorkspace.allocate(std.testing.allocator, .{
5116 .header = recoveredHeader(),
5117 .max_wal_bytes = default_max_wal_bytes,
5118 .path_storage = .{ .database_bytes = 0, .wal_bytes = 0 },
5119 });
5120 defer workspace.deallocate(std.testing.allocator);
5121 var recovered = try recover(std.testing.allocator, &workspace, tmp.dir, .{
5122 .paths = .{ .database = "main.db", .wal = "main.wal" },
5123 .header = recoveredHeader(),
5124 });
5125 defer workspace.release(&recovered);
5126
5127 const snapshot = try recovered.beginRead();
5128 try std.testing.expectEqual(@as(u8, 20), (try snapshot.get(1)).?[1]);
5129 }
5130
5131 test "file recovery reuses clean wal header" {
5132 var tmp = std.testing.tmpDir(.{});
5133 defer tmp.cleanup();
5134
5135 var source = try testingPager(0);
5136 defer deinitTestingPager(&source);
5137
5138 var base: [page.size]u8 = undefined;
5139 fillImage(&base, 1, 10);
5140
5141 try source.installBase(1, &base);
5142 try writeBase(testing_io, tmp.dir, "main.db", &source);
5143 try writeWal(testing_io, tmp.dir, "main.wal", &source);
5144
5145 var workspace = try DatabaseWorkspace.allocate(std.testing.allocator, .{
5146 .header = testingHeader(),
5147 .max_wal_bytes = default_max_wal_bytes,
5148 .path_storage = .{ .database_bytes = 0, .wal_bytes = 0 },
5149 });
5150 defer workspace.deallocate(std.testing.allocator);
5151 var recovered = try recoverState(std.testing.allocator, &workspace, tmp.dir, .{
5152 .paths = .{ .database = "main.db", .wal = "main.wal" },
5153 .header = testingHeader(),
5154 });
5155 defer recovered.deinit();
5156
5157 try std.testing.expect(!recovered.rewrite_base);
5158 try std.testing.expect(!recovered.rewrite_wal);
5159 }
5160
5161 test "file recovery rewrites stale wal header without base rewrite" {
5162 var tmp = std.testing.tmpDir(.{});
5163 defer tmp.cleanup();
5164
5165 var source = try testingPager(0);
5166 defer deinitTestingPager(&source);
5167
5168 var base: [page.size]u8 = undefined;
5169 fillImage(&base, 1, 10);
5170
5171 try source.installBase(1, &base);
5172 try writeBase(testing_io, tmp.dir, "main.db", &source);
5173 try writeWal(testing_io, tmp.dir, "main.wal", &source);
5174
5175 var workspace = try DatabaseWorkspace.allocate(std.testing.allocator, .{
5176 .header = recoveredHeader(),
5177 .max_wal_bytes = default_max_wal_bytes,
5178 .path_storage = .{ .database_bytes = 0, .wal_bytes = 0 },
5179 });
5180 defer workspace.deallocate(std.testing.allocator);
5181 var recovered = try recoverState(std.testing.allocator, &workspace, tmp.dir, .{
5182 .paths = .{ .database = "main.db", .wal = "main.wal" },
5183 .header = recoveredHeader(),
5184 });
5185 defer recovered.deinit();
5186
5187 try std.testing.expect(!recovered.rewrite_base);
5188 try std.testing.expect(recovered.rewrite_wal);
5189 }
5190
5191 test "file database opens clean base pages lazily" {
5192 var tmp = std.testing.tmpDir(.{});
5193 defer tmp.cleanup();
5194
5195 var source = try testingPager(0);
5196 defer deinitTestingPager(&source);
5197
5198 var first: [page.size]u8 = undefined;
5199 var second: [page.size]u8 = undefined;
5200 fillImage(&first, 1, 11);
5201 fillImage(&second, 2, 22);
5202
5203 try source.installBase(1, &first);
5204 try source.installBase(2, &second);
5205 try writeBase(testing_io, tmp.dir, "main.db", &source);
5206 try writeWal(testing_io, tmp.dir, "main.wal", &source);
5207
5208 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
5209 .paths = .{ .database = "main.db", .wal = "main.wal" },
5210 .header = testingHeader(),
5211 });
5212 defer database.deinit();
5213
5214 try std.testing.expect(!database.base_loaded);
5215 try std.testing.expectEqual(@as(usize, 0), database.pager.base.items.len);
5216 try std.testing.expectEqual(@as(u32, 2), database.pager.databasePageCount());
5217
5218 try std.testing.expectEqual(
5219 @as(u8, 22),
5220 (try testDatabasePageByte(&database, 2)).?,
5221 );
5222 try std.testing.expectEqual(@as(usize, 0), database.pager.base.items.len);
5223 try std.testing.expectEqual(@as(usize, 1), database.read_cache.count());
5224 }
5225
5226 test "file database read-only open replays committed wal without repair effects" {
5227 var tmp = std.testing.tmpDir(.{});
5228 defer tmp.cleanup();
5229
5230 var source = try testingPager(2);
5231 defer deinitTestingPager(&source);
5232
5233 var base: [page.size]u8 = undefined;
5234 var committed: [page.size]u8 = undefined;
5235 var uncommitted: [page.size]u8 = undefined;
5236 fillImage(&base, 1, 10);
5237 fillImage(&committed, 1, 20);
5238 fillImage(&uncommitted, 1, 99);
5239 try source.installBase(1, &base);
5240 try source.appendWal(1, 1, &committed);
5241 try source.appendWal(1, 0, &uncommitted);
5242 try writeBase(testing_io, tmp.dir, "main.db", &source);
5243 try writeWal(testing_io, tmp.dir, "main.wal", &source);
5244 {
5245 var sidecar = try tmp.dir.createFile(testing_io, "main.wal.next", .{ .read = true, .truncate = true });
5246 defer sidecar.close(testing_io);
5247 try sidecar.writePositionalAll(testing_io, "preserve", 0);
5248 }
5249
5250 const base_before = try tmp.dir.readFileAlloc(testing_io, "main.db", std.testing.allocator, .unlimited);
5251 defer std.testing.allocator.free(base_before);
5252 const wal_before = try tmp.dir.readFileAlloc(testing_io, "main.wal", std.testing.allocator, .unlimited);
5253 defer std.testing.allocator.free(wal_before);
5254 const sidecar_before = try tmp.dir.readFileAlloc(testing_io, "main.wal.next", std.testing.allocator, .unlimited);
5255 defer std.testing.allocator.free(sidecar_before);
5256 const base_stat = try tmp.dir.statFile(testing_io, "main.db", .{});
5257 const wal_stat = try tmp.dir.statFile(testing_io, "main.wal", .{});
5258 const sidecar_stat = try tmp.dir.statFile(testing_io, "main.wal.next", .{});
5259
5260 {
5261 var database = try openReadOnlyForTest(tmp.dir, .{
5262 .paths = .{ .database = "main.db", .wal = "main.wal" },
5263 .header = testingHeader(),
5264 });
5265 defer database.deinit();
5266 const snapshot = database.snapshot();
5267 try std.testing.expectEqual(@as(u8, 20), (try testSnapshotPageByte(snapshot, 1)).?);
5268 try std.testing.expect(!@hasDecl(ReadOnlyDatabase, "beginWrite"));
5269 try std.testing.expect(!@hasDecl(ReadOnlyDatabase, "syncWal"));
5270 try std.testing.expect(!@hasDecl(ReadOnlyDatabase, "checkpoint"));
5271 try std.testing.expect(!@hasField(ReadOnlyDatabase, "pager"));
5272 }
5273
5274 const base_after = try tmp.dir.readFileAlloc(testing_io, "main.db", std.testing.allocator, .unlimited);
5275 defer std.testing.allocator.free(base_after);
5276 const wal_after = try tmp.dir.readFileAlloc(testing_io, "main.wal", std.testing.allocator, .unlimited);
5277 defer std.testing.allocator.free(wal_after);
5278 const sidecar_after = try tmp.dir.readFileAlloc(testing_io, "main.wal.next", std.testing.allocator, .unlimited);
5279 defer std.testing.allocator.free(sidecar_after);
5280 try std.testing.expectEqualSlices(u8, base_before, base_after);
5281 try std.testing.expectEqualSlices(u8, wal_before, wal_after);
5282 try std.testing.expectEqualSlices(u8, sidecar_before, sidecar_after);
5283 try std.testing.expectEqual(base_stat.mtime.nanoseconds, (try tmp.dir.statFile(testing_io, "main.db", .{})).mtime.nanoseconds);
5284 try std.testing.expectEqual(wal_stat.mtime.nanoseconds, (try tmp.dir.statFile(testing_io, "main.wal", .{})).mtime.nanoseconds);
5285 try std.testing.expectEqual(sidecar_stat.mtime.nanoseconds, (try tmp.dir.statFile(testing_io, "main.wal.next", .{})).mtime.nanoseconds);
5286 try std.testing.expectEqual(@backingInt(base_stat.permissions), @backingInt((try tmp.dir.statFile(testing_io, "main.db", .{})).permissions));
5287 try std.testing.expectEqual(@backingInt(wal_stat.permissions), @backingInt((try tmp.dir.statFile(testing_io, "main.wal", .{})).permissions));
5288 try std.testing.expectEqual(@backingInt(sidecar_stat.permissions), @backingInt((try tmp.dir.statFile(testing_io, "main.wal.next", .{})).permissions));
5289 }
5290
5291 test "file database read-only recovery honors control throughout wal replay" {
5292 var tmp = std.testing.tmpDir(.{});
5293 defer tmp.cleanup();
5294 const frames_max: usize = 4;
5295 const max_wal_bytes = wal.header_size + frames_max * wal.frame_size;
5296 var source = try testingPager(frames_max);
5297 defer deinitTestingPager(&source);
5298 var image: [page.size]u8 = undefined;
5299 var page_id: u32 = 1;
5300 while (page_id <= frames_max) : (page_id += 1) {
5301 fillImage(&image, page_id, @intCast(80 + page_id));
5302 try source.appendWal(page_id, if (page_id == frames_max) page_id else 0, &image);
5303 }
5304 try writeWal(testing_io, tmp.dir, "main.wal", &source);
5305
5306 var workspace = try ReadOnlyDatabase.Workspace.allocate(std.testing.allocator, .{
5307 .header = testingHeader(),
5308 .max_wal_bytes = max_wal_bytes,
5309 .path_storage = .{ .database_bytes = 0, .wal_bytes = 0 },
5310 .read_cache_pages = default_read_cache_capacity,
5311 });
5312 defer workspace.deallocate(std.testing.allocator);
5313 var probe = TestingInterrupt{ .remaining = 0, .enabled = false };
5314 var first = try openReadOnlyWithWorkspaceForTest(
5315 &workspace,
5316 tmp.dir,
5317 .{
5318 .paths = .{ .database = "missing.db", .wal = "main.wal" },
5319 .header = testingHeader(),
5320 .max_wal_bytes = max_wal_bytes,
5321 .control = probe.control(),
5322 },
5323 );
5324 first.deinit();
5325 const attempts_max = std.math.add(usize, probe.calls, 1) catch unreachable;
5326 var budget: usize = 0;
5327 while (budget < attempts_max) : (budget += 1) {
5328 var interrupt = TestingInterrupt{ .remaining = budget };
5329 var database = openReadOnlyWithWorkspaceForTest(
5330 &workspace,
5331 tmp.dir,
5332 .{
5333 .paths = .{ .database = "missing.db", .wal = "main.wal" },
5334 .header = testingHeader(),
5335 .max_wal_bytes = max_wal_bytes,
5336 .control = interrupt.control(),
5337 },
5338 ) catch |err| switch (err) {
5339 error.Interrupted => continue,
5340 else => return err,
5341 };
5342 database.deinit();
5343 break;
5344 }
5345 try std.testing.expect(budget > frames_max * 2);
5346 try std.testing.expectEqual(probe.calls, budget);
5347 }
5348
5349 test "file database read-only open reports repair required for invalid wal" {
5350 var tmp = std.testing.tmpDir(.{});
5351 defer tmp.cleanup();
5352
5353 var source = try testingPager(0);
5354 defer deinitTestingPager(&source);
5355 var base: [page.size]u8 = undefined;
5356 fillImage(&base, 1, 10);
5357 try source.installBase(1, &base);
5358 try writeBase(testing_io, tmp.dir, "main.db", &source);
5359 {
5360 var wal_file = try tmp.dir.createFile(testing_io, "main.wal", .{ .read = true, .truncate = true });
5361 defer wal_file.close(testing_io);
5362 try wal_file.writePositionalAll(testing_io, "invalid", 0);
5363 }
5364
5365 try expectReadRepair(tmp.dir, .{
5366 .paths = .{ .database = "main.db", .wal = "main.wal" },
5367 .header = testingHeader(),
5368 }, .invalid_wal);
5369 }
5370
5371 test "file database read-only open materializes stale-header recovery without rewrite" {
5372 var tmp = std.testing.tmpDir(.{});
5373 defer tmp.cleanup();
5374
5375 var source = try testingPager(1);
5376 defer deinitTestingPager(&source);
5377 var base: [page.size]u8 = undefined;
5378 var committed: [page.size]u8 = undefined;
5379 fillImage(&base, 1, 10);
5380 fillImage(&committed, 1, 20);
5381 try source.installBase(1, &base);
5382 try source.appendWal(1, 1, &committed);
5383 try writeBase(testing_io, tmp.dir, "main.db", &source);
5384 try writeWal(testing_io, tmp.dir, "main.wal", &source);
5385 const base_before = try tmp.dir.readFileAlloc(testing_io, "main.db", std.testing.allocator, .unlimited);
5386 defer std.testing.allocator.free(base_before);
5387 const wal_before = try tmp.dir.readFileAlloc(testing_io, "main.wal", std.testing.allocator, .unlimited);
5388 defer std.testing.allocator.free(wal_before);
5389
5390 var database = try openReadOnlyForTest(tmp.dir, .{
5391 .paths = .{ .database = "main.db", .wal = "main.wal" },
5392 .header = recoveredHeader(),
5393 });
5394 defer database.deinit();
5395 try std.testing.expectEqual(@as(u8, 20), (try testSnapshotPageByte(database.snapshot(), 1)).?);
5396 const base_after = try tmp.dir.readFileAlloc(testing_io, "main.db", std.testing.allocator, .unlimited);
5397 defer std.testing.allocator.free(base_after);
5398 const wal_after = try tmp.dir.readFileAlloc(testing_io, "main.wal", std.testing.allocator, .unlimited);
5399 defer std.testing.allocator.free(wal_after);
5400 try std.testing.expectEqualSlices(u8, base_before, base_after);
5401 try std.testing.expectEqualSlices(u8, wal_before, wal_after);
5402 }
5403
5404 test "file database read-only open uses complete base when wal is missing" {
5405 var tmp = std.testing.tmpDir(.{});
5406 defer tmp.cleanup();
5407
5408 var source = try testingPager(0);
5409 defer deinitTestingPager(&source);
5410 var base: [page.size]u8 = undefined;
5411 fillImage(&base, 1, 31);
5412 try source.installBase(1, &base);
5413 try writeBase(testing_io, tmp.dir, "main.db", &source);
5414
5415 var database = try openReadOnlyForTest(tmp.dir, .{
5416 .paths = .{ .database = "main.db", .wal = "missing.wal" },
5417 .header = testingHeader(),
5418 });
5419 defer database.deinit();
5420 try std.testing.expectEqual(@as(u8, 31), (try testSnapshotPageByte(database.snapshot(), 1)).?);
5421 try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(testing_io, "missing.wal", .{}));
5422 }
5423
5424 test "file database read-only open accepts header-only wal with missing base" {
5425 var tmp = std.testing.tmpDir(.{});
5426 defer tmp.cleanup();
5427
5428 var source = try testingPager(0);
5429 defer deinitTestingPager(&source);
5430 try writeWal(testing_io, tmp.dir, "main.wal", &source);
5431
5432 var database = try openReadOnlyForTest(tmp.dir, .{
5433 .paths = .{ .database = "missing.db", .wal = "main.wal" },
5434 .header = testingHeader(),
5435 });
5436 defer database.deinit();
5437 try std.testing.expect((try testSnapshotPageByte(database.snapshot(), 1)) == null);
5438 try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(testing_io, "missing.db", .{}));
5439 }
5440
5441 test "file database read-only open reports missing snapshot" {
5442 var tmp = std.testing.tmpDir(.{});
5443 defer tmp.cleanup();
5444
5445 try expectReadRepair(tmp.dir, .{
5446 .paths = .{ .database = "missing.db", .wal = "missing.wal" },
5447 .header = testingHeader(),
5448 }, .missing_snapshot);
5449 }
5450
5451 test "file database read-only open requires complete wal coverage without base" {
5452 var tmp = std.testing.tmpDir(.{});
5453 defer tmp.cleanup();
5454
5455 var covered = try testingPager(2);
5456 defer deinitTestingPager(&covered);
5457 var first: [page.size]u8 = undefined;
5458 var second: [page.size]u8 = undefined;
5459 fillImage(&first, 1, 41);
5460 fillImage(&second, 2, 42);
5461 try covered.appendWal(1, 0, &first);
5462 try covered.appendWal(2, 2, &second);
5463 try writeWal(testing_io, tmp.dir, "covered.wal", &covered);
5464
5465 var database = try openReadOnlyForTest(tmp.dir, .{
5466 .paths = .{ .database = "missing.db", .wal = "covered.wal" },
5467 .header = testingHeader(),
5468 });
5469 defer database.deinit();
5470 try std.testing.expectEqual(@as(u8, 41), (try testSnapshotPageByte(database.snapshot(), 1)).?);
5471 try std.testing.expectEqual(@as(u8, 42), (try testSnapshotPageByte(database.snapshot(), 2)).?);
5472
5473 var uncovered = try testingPager(1);
5474 defer deinitTestingPager(&uncovered);
5475 try uncovered.appendWal(2, 2, &second);
5476 try writeWal(testing_io, tmp.dir, "uncovered.wal", &uncovered);
5477 try expectReadRepair(tmp.dir, .{
5478 .paths = .{ .database = "missing.db", .wal = "uncovered.wal" },
5479 .header = testingHeader(),
5480 }, .invalid_database);
5481 }
5482
5483 test "file database read-only open recovers only a covered torn base tail" {
5484 var tmp = std.testing.tmpDir(.{});
5485 defer tmp.cleanup();
5486
5487 {
5488 var covered = try testingPager(1);
5489 defer deinitTestingPager(&covered);
5490 var committed: [page.size]u8 = undefined;
5491 fillImage(&committed, 2, 52);
5492 try covered.appendWal(2, 2, &committed);
5493 try writeWal(testing_io, tmp.dir, "covered.wal", &covered);
5494 }
5495 {
5496 var base = try tmp.dir.createFile(testing_io, "covered.db", .{ .read = true, .truncate = true });
5497 defer base.close(testing_io);
5498 const torn = @as([(page.size + 1)]u8, @splat(0));
5499 try base.writePositionalAll(testing_io, torn[0..], 0);
5500 }
5501
5502 var database = try openReadOnlyForTest(tmp.dir, .{
5503 .paths = .{ .database = "covered.db", .wal = "covered.wal" },
5504 .header = testingHeader(),
5505 });
5506 defer database.deinit();
5507 try std.testing.expectEqual(@as(u8, 52), (try testSnapshotPageByte(database.snapshot(), 2)).?);
5508
5509 {
5510 var uncovered = try testingPager(1);
5511 defer deinitTestingPager(&uncovered);
5512 var committed: [page.size]u8 = undefined;
5513 fillImage(&committed, 1, 51);
5514 try uncovered.appendWal(1, 1, &committed);
5515 try writeWal(testing_io, tmp.dir, "uncovered.wal", &uncovered);
5516 }
5517 {
5518 var base = try tmp.dir.createFile(testing_io, "uncovered.db", .{ .read = true, .truncate = true });
5519 defer base.close(testing_io);
5520 const torn = @as([(page.size + 1)]u8, @splat(0));
5521 try base.writePositionalAll(testing_io, torn[0..], 0);
5522 }
5523 try expectReadRepair(tmp.dir, .{
5524 .paths = .{ .database = "uncovered.db", .wal = "uncovered.wal" },
5525 .header = testingHeader(),
5526 }, .invalid_database);
5527 }
5528
5529 test "file database read-only open discards a torn transaction batch" {
5530 var tmp = std.testing.tmpDir(.{});
5531 defer tmp.cleanup();
5532
5533 {
5534 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
5535 .paths = .{ .database = "main.db", .wal = "main.wal" },
5536 .header = testingHeader(),
5537 });
5538 defer database.deinit();
5539 var first: [page.size]u8 = undefined;
5540 var second: [page.size]u8 = undefined;
5541 var third: [page.size]u8 = undefined;
5542 fillImage(&first, 1, 61);
5543 fillImage(&second, 2, 62);
5544 fillImage(&third, 3, 63);
5545 var stable = try database.beginWrite();
5546 defer stable.deinit();
5547 try stable.putPage(1, &first);
5548 _ = try stable.commit(.{});
5549 var torn = try database.beginWrite();
5550 defer torn.deinit();
5551 try torn.putPage(2, &second);
5552 try torn.putPage(3, &third);
5553 _ = try torn.commit(.{});
5554 }
5555 {
5556 var wal_file = try tmp.dir.openFile(testing_io, "main.wal", .{ .mode = .read_write });
5557 defer wal_file.close(testing_io);
5558 try wal_file.setLength(testing_io, wal.header_size + 2 * wal.frame_size + wal.frame_header_size + 97);
5559 }
5560
5561 var database = try openReadOnlyForTest(tmp.dir, .{
5562 .paths = .{ .database = "main.db", .wal = "main.wal" },
5563 .header = testingHeader(),
5564 });
5565 defer database.deinit();
5566 const snapshot = database.snapshot();
5567 try std.testing.expectEqual(@as(u8, 61), (try testSnapshotPageByte(snapshot, 1)).?);
5568 try std.testing.expect((try testSnapshotPageByte(snapshot, 2)) == null);
5569 try std.testing.expect((try testSnapshotPageByte(snapshot, 3)) == null);
5570 }
5571
5572 test "file database read-only open keeps base reads lazy and cache bounded" {
5573 var tmp = std.testing.tmpDir(.{});
5574 defer tmp.cleanup();
5575
5576 var source = try testingPager(0);
5577 defer deinitTestingPager(&source);
5578 var image: [page.size]u8 = undefined;
5579 for (1..4) |page_id| {
5580 fillImage(&image, @intCast(page_id), @intCast(70 + page_id));
5581 try source.installBase(@intCast(page_id), &image);
5582 }
5583 try writeBase(testing_io, tmp.dir, "main.db", &source);
5584
5585 var database = try openReadOnlyForTest(tmp.dir, .{
5586 .paths = .{ .database = "main.db", .wal = "missing.wal" },
5587 .header = testingHeader(),
5588 .read_cache_capacity = 1,
5589 });
5590 defer database.deinit();
5591 try std.testing.expectEqual(@as(usize, 0), database.impl().pager.base.items.len);
5592 const snapshot = database.snapshot();
5593 try std.testing.expectEqual(@as(u8, 71), (try testSnapshotPageByte(snapshot, 1)).?);
5594 try std.testing.expectEqual(@as(u8, 72), (try testSnapshotPageByte(snapshot, 2)).?);
5595 try std.testing.expectEqual(@as(usize, 1), database.impl().read_cache.count());
5596 try std.testing.expectEqual(@as(usize, 0), database.impl().pager.base.items.len);
5597 }
5598
5599 test "file database read-only open handles the maximum sparse base length" {
5600 var tmp = std.testing.tmpDir(.{});
5601 defer tmp.cleanup();
5602
5603 var source = try testingPager(0);
5604 defer deinitTestingPager(&source);
5605 try writeWal(testing_io, tmp.dir, "main.wal", &source);
5606 var base = try tmp.dir.createFile(testing_io, "main.db", .{ .read = true, .truncate = true });
5607 defer base.close(testing_io);
5608 const maximum_length = @as(u64, std.math.maxInt(u32)) * page.size;
5609 try base.setLength(testing_io, maximum_length);
5610 {
5611 var database = try openReadOnlyForTest(tmp.dir, .{
5612 .paths = .{ .database = "main.db", .wal = "main.wal" },
5613 .header = testingHeader(),
5614 });
5615 database.deinit();
5616 }
5617 }
5618
5619 test "file sparse snapshot refresh reads only appended wal frames" {
5620 var tmp = std.testing.tmpDir(.{});
5621 defer tmp.cleanup();
5622 const paths = Paths{ .database = "main.db", .wal = "main.wal" };
5623 var writer = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
5624 .paths = paths,
5625 .header = testingHeader(),
5626 });
5627 defer writer.deinit();
5628 try commitTestPage(&writer, 1, 41);
5629 try commitTestPage(&writer, 2, 42);
5630
5631 var scratch: [wal.frame_size]u8 = undefined;
5632 var refs: [8]wal.FrameRef = undefined;
5633 var probe = SparseReadProbe{};
5634 var reader = try openSparseReadOnlyForTest(tmp.dir, paths, &scratch, &refs, &probe);
5635 defer reader.deinit();
5636 const oldest = reader.snapshot();
5637 try std.testing.expectEqual(@as(u8, 41), (try testSnapshotPageByte(oldest, 1)).?);
5638 try std.testing.expectEqual(@as(u8, 42), (try testSnapshotPageByte(oldest, 2)).?);
5639
5640 try commitTestPage(&writer, 1, 82);
5641 const bytes_before_refresh = probe.wal_bytes;
5642 try std.testing.expectEqual(
5643 std.meta.Tag(SparseRefreshResult).refreshed,
5644 std.meta.activeTag(try reader.refresh(tmp.dir, .{ .paths = paths })),
5645 );
5646 try std.testing.expectEqual(wal.frame_size, probe.wal_bytes - bytes_before_refresh);
5647 var stale_image: [page.size]u8 = undefined;
5648 try std.testing.expectError(error.RecoveryRequired, oldest.copyPage(1, &stale_image));
5649 const current = reader.snapshot();
5650 try std.testing.expectEqual(@as(u8, 82), (try testSnapshotPageByte(current, 1)).?);
5651 try std.testing.expectEqual(@as(u8, 42), (try testSnapshotPageByte(current, 2)).?);
5652 }
5653
5654 test "file sparse snapshot reopens a checkpoint generation" {
5655 var tmp = std.testing.tmpDir(.{});
5656 defer tmp.cleanup();
5657 const paths = Paths{ .database = "main.db", .wal = "main.wal" };
5658 var writer = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
5659 .paths = paths,
5660 .header = testingHeader(),
5661 });
5662 defer writer.deinit();
5663 try commitTestPage(&writer, 1, 31);
5664
5665 var scratch: [wal.frame_size]u8 = undefined;
5666 var refs: [4]wal.FrameRef = undefined;
5667 var probe = SparseReadProbe{};
5668 var reader = try openSparseReadOnlyForTest(tmp.dir, paths, &scratch, &refs, &probe);
5669 defer reader.deinit();
5670 const oldest = reader.snapshot();
5671 try commitTestPage(&writer, 1, 62);
5672 _ = try writer.checkpoint(.{ .restart_header = recoveredHeader() });
5673 try std.testing.expectEqual(
5674 std.meta.Tag(SparseRefreshResult).refreshed,
5675 std.meta.activeTag(try reader.refresh(tmp.dir, .{ .paths = paths })),
5676 );
5677 var stale_image: [page.size]u8 = undefined;
5678 try std.testing.expectError(error.RecoveryRequired, oldest.copyPage(1, &stale_image));
5679 try std.testing.expectEqual(
5680 @as(u8, 62),
5681 (try testSnapshotPageByte(reader.snapshot(), 1)).?,
5682 );
5683 try std.testing.expectEqual(@as(usize, 1), probe.base_reads);
5684 }
5685
5686 test "file sparse snapshot reads a base without wal" {
5687 var tmp = std.testing.tmpDir(.{});
5688 defer tmp.cleanup();
5689 const paths = Paths{ .database = "main.db", .wal = "missing.wal" };
5690 var source = try testingPager(0);
5691 defer deinitTestingPager(&source);
5692 var image: [page.size]u8 = undefined;
5693 fillImage(&image, 1, 41);
5694 try source.installBase(1, &image);
5695 try writeBase(testing_io, tmp.dir, paths.database, &source);
5696
5697 var scratch: [wal.frame_size]u8 = undefined;
5698 var refs: [0]wal.FrameRef = .{};
5699 var reader = try openSparseReadOnlyForTest(tmp.dir, paths, &scratch, &refs, null);
5700 defer reader.deinit();
5701 try std.testing.expectEqual(@as(u8, 41), (try testSnapshotPageByte(reader.snapshot(), 1)).?);
5702 try std.testing.expectEqual(
5703 std.meta.Tag(SparseRefreshResult).unchanged,
5704 std.meta.activeTag(try reader.refresh(tmp.dir, .{ .paths = paths })),
5705 );
5706 var interrupt = TestingInterrupt{ .remaining = 0 };
5707 try std.testing.expectError(
5708 error.Interrupted,
5709 reader.refresh(tmp.dir, .{
5710 .paths = paths,
5711 .control = interrupt.control(),
5712 }),
5713 );
5714 }
5715
5716 test "file sparse snapshot rejects wal capacity before payload reads" {
5717 var tmp = std.testing.tmpDir(.{});
5718 defer tmp.cleanup();
5719 const paths = Paths{ .database = "main.db", .wal = "main.wal" };
5720 var writer = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
5721 .paths = paths,
5722 .header = testingHeader(),
5723 });
5724 defer writer.deinit();
5725 try commitTestPage(&writer, 1, 41);
5726 try commitTestPage(&writer, 2, 42);
5727
5728 var scratch: [wal.frame_size]u8 = undefined;
5729 var refs: [1]wal.FrameRef = undefined;
5730 var probe = SparseReadProbe{};
5731 try expectSparseReadRepair(
5732 tmp.dir,
5733 paths,
5734 &scratch,
5735 &refs,
5736 &probe,
5737 .wal_too_large,
5738 );
5739 try std.testing.expectEqual(@as(usize, 0), probe.wal_reads);
5740 try std.testing.expectEqual(@as(usize, 0), probe.wal_bytes);
5741 }
5742
5743 test "file sparse snapshot cancellation precedes wal payload reads" {
5744 var tmp = std.testing.tmpDir(.{});
5745 defer tmp.cleanup();
5746 const paths = Paths{ .database = "main.db", .wal = "main.wal" };
5747 var writer = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
5748 .paths = paths,
5749 .header = testingHeader(),
5750 });
5751 defer writer.deinit();
5752 try commitTestPage(&writer, 1, 41);
5753
5754 var scratch: [wal.frame_size]u8 = undefined;
5755 var refs: [1]wal.FrameRef = undefined;
5756 var probe = SparseReadProbe{};
5757 var interrupt = TestingInterrupt{ .remaining = 0 };
5758 try std.testing.expectError(
5759 error.Interrupted,
5760 SparseReadOnlyDatabase.openExisting(tmp.dir, .{
5761 .paths = paths,
5762 .workspace = .{ .scratch = &scratch, .refs = &refs },
5763 .control = interrupt.control(),
5764 .probe = &probe,
5765 }),
5766 );
5767 try std.testing.expectEqual(@as(usize, 0), probe.wal_reads);
5768 try std.testing.expectEqual(@as(usize, 0), probe.wal_bytes);
5769
5770 var reader = try openSparseReadOnlyForTest(tmp.dir, paths, &scratch, &refs, null);
5771 defer reader.deinit();
5772 try std.testing.expectEqual(@as(u8, 41), (try testSnapshotPageByte(reader.snapshot(), 1)).?);
5773 }
5774
5775 test "file sparse snapshot ignores an incomplete transaction tail" {
5776 var tmp = std.testing.tmpDir(.{});
5777 defer tmp.cleanup();
5778 const paths = Paths{ .database = "main.db", .wal = "main.wal" };
5779 var source = try testingPager(2);
5780 defer deinitTestingPager(&source);
5781 var base: [page.size]u8 = undefined;
5782 var committed: [page.size]u8 = undefined;
5783 var uncommitted: [page.size]u8 = undefined;
5784 fillImage(&base, 1, 10);
5785 fillImage(&committed, 1, 41);
5786 fillImage(&uncommitted, 1, 99);
5787 try source.installBase(1, &base);
5788 try source.appendWal(1, 1, &committed);
5789 try source.appendWal(1, 0, &uncommitted);
5790 try writeBase(testing_io, tmp.dir, paths.database, &source);
5791 try writeWal(testing_io, tmp.dir, paths.wal, &source);
5792 {
5793 var wal_file = try tmp.dir.openFile(testing_io, paths.wal, .{ .mode = .read_write });
5794 defer wal_file.close(testing_io);
5795 try wal_file.setLength(
5796 testing_io,
5797 wal.header_size + wal.frame_size + wal.frame_header_size + 97,
5798 );
5799 }
5800
5801 var scratch: [wal.frame_size]u8 = undefined;
5802 var refs: [2]wal.FrameRef = undefined;
5803 var reader = try openSparseReadOnlyForTest(tmp.dir, paths, &scratch, &refs, null);
5804 defer reader.deinit();
5805 const snapshot = reader.snapshot();
5806 try std.testing.expectEqual(@as(u8, 41), (try testSnapshotPageByte(snapshot, 1)).?);
5807 try std.testing.expect((try testSnapshotPageByte(snapshot, 2)) == null);
5808 }
5809
5810 test "file sparse snapshot interruption invalidates an incremental refresh" {
5811 var tmp = std.testing.tmpDir(.{});
5812 defer tmp.cleanup();
5813 const paths = Paths{ .database = "main.db", .wal = "main.wal" };
5814 var writer = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
5815 .paths = paths,
5816 .header = testingHeader(),
5817 });
5818 defer writer.deinit();
5819 try commitTestPage(&writer, 1, 41);
5820
5821 var scratch: [wal.frame_size]u8 = undefined;
5822 var refs: [2]wal.FrameRef = undefined;
5823 var probe = SparseReadProbe{};
5824 var reader = try openSparseReadOnlyForTest(tmp.dir, paths, &scratch, &refs, &probe);
5825 defer reader.deinit();
5826 const oldest = reader.snapshot();
5827 try commitTestPage(&writer, 1, 82);
5828 const bytes_before_refresh = probe.wal_bytes;
5829 var interrupt = TestingInterrupt{ .remaining = 3 };
5830 try std.testing.expectError(
5831 error.Interrupted,
5832 reader.refresh(tmp.dir, .{
5833 .paths = paths,
5834 .control = interrupt.control(),
5835 }),
5836 );
5837 try std.testing.expectEqual(bytes_before_refresh, probe.wal_bytes);
5838 var stale_image: [page.size]u8 = undefined;
5839 try std.testing.expectError(error.RecoveryRequired, oldest.copyPage(1, &stale_image));
5840 try std.testing.expectError(
5841 error.RecoveryRequired,
5842 reader.refresh(tmp.dir, .{ .paths = paths }),
5843 );
5844
5845 reader.deinit();
5846 reader = try openSparseReadOnlyForTest(tmp.dir, paths, &scratch, &refs, &probe);
5847 try std.testing.expectEqual(@as(u8, 82), (try testSnapshotPageByte(reader.snapshot(), 1)).?);
5848 }
5849
5850 test "file sparse snapshot verifies a selected wal frame before copying" {
5851 var tmp = std.testing.tmpDir(.{});
5852 defer tmp.cleanup();
5853 const paths = Paths{ .database = "main.db", .wal = "main.wal" };
5854 var writer = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
5855 .paths = paths,
5856 .header = testingHeader(),
5857 });
5858 defer writer.deinit();
5859 try commitTestPage(&writer, 1, 41);
5860
5861 var scratch: [wal.frame_size]u8 = undefined;
5862 var refs: [1]wal.FrameRef = undefined;
5863 var reader = try openSparseReadOnlyForTest(tmp.dir, paths, &scratch, &refs, null);
5864 defer reader.deinit();
5865 const snapshot = reader.snapshot();
5866 var wal_file = try tmp.dir.openFile(testing_io, paths.wal, .{ .mode = .read_write });
5867 defer wal_file.close(testing_io);
5868 try wal_file.writePositionalAll(
5869 testing_io,
5870 &.{99},
5871 wal.frameOffset(1) + wal.frame_header_size + 1,
5872 );
5873 var destination: [page.size]u8 = @splat(0xa5);
5874 try std.testing.expectError(error.RecoveryRequired, snapshot.copyPage(1, &destination));
5875 try std.testing.expectEqualSlices(u8, &@as([page.size]u8, @splat(0xa5)), &destination);
5876 }
5877
5878 test "file database read-only open reports a wal inspection bound" {
5879 var tmp = std.testing.tmpDir(.{});
5880 defer tmp.cleanup();
5881
5882 var source = try testingPager(1);
5883 defer deinitTestingPager(&source);
5884 var image: [page.size]u8 = undefined;
5885 fillImage(&image, 1, 42);
5886 try source.appendWal(1, 1, &image);
5887 try writeWal(testing_io, tmp.dir, "main.wal", &source);
5888 try expectReadRepair(tmp.dir, .{
5889 .paths = .{ .database = "missing.db", .wal = "main.wal" },
5890 .header = testingHeader(),
5891 .max_wal_bytes = wal.header_size,
5892 }, .wal_too_large);
5893 }
5894
5895 test "file database owns paths needed by later checkpoints" {
5896 var tmp = std.testing.tmpDir(.{});
5897 defer tmp.cleanup();
5898
5899 const database_path = try std.testing.allocator.dupe(u8, "owned.db");
5900 const wal_path = std.testing.allocator.dupe(u8, "owned.wal") catch |err| {
5901 std.testing.allocator.free(database_path);
5902 return err;
5903 };
5904 var input_paths_live = true;
5905 defer if (input_paths_live) {
5906 std.testing.allocator.free(database_path);
5907 std.testing.allocator.free(wal_path);
5908 };
5909 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
5910 .paths = .{ .database = database_path, .wal = wal_path },
5911 .header = testingHeader(),
5912 });
5913 std.testing.allocator.free(database_path);
5914 std.testing.allocator.free(wal_path);
5915 input_paths_live = false;
5916 defer database.deinit();
5917
5918 var image: [page.size]u8 = undefined;
5919 fillImage(&image, 1, 42);
5920 try database.appendWalSync(1, 1, &image);
5921 const checkpoint = try database.checkpoint(.{ .restart_header = testingHeader() });
5922 try std.testing.expect(checkpoint.restarted);
5923 }
5924
5925 test "file database opens matching committed wal lazily" {
5926 var tmp = std.testing.tmpDir(.{});
5927 defer tmp.cleanup();
5928
5929 var source = try testingPager(1);
5930 defer deinitTestingPager(&source);
5931
5932 var first: [page.size]u8 = undefined;
5933 var second: [page.size]u8 = undefined;
5934 var committed: [page.size]u8 = undefined;
5935 fillImage(&first, 1, 11);
5936 fillImage(&second, 2, 22);
5937 fillImage(&committed, 1, 99);
5938
5939 try source.installBase(1, &first);
5940 try source.installBase(2, &second);
5941 try source.appendWal(1, 2, &committed);
5942 try writeBase(testing_io, tmp.dir, "main.db", &source);
5943 try writeWal(testing_io, tmp.dir, "main.wal", &source);
5944
5945 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
5946 .paths = .{ .database = "main.db", .wal = "main.wal" },
5947 .header = testingHeader(),
5948 });
5949 defer database.deinit();
5950
5951 try std.testing.expect(!database.base_loaded);
5952 try std.testing.expectEqual(@as(usize, 0), database.pager.base.items.len);
5953 try std.testing.expectEqual(@as(usize, 1), database.pager.frameCount());
5954
5955 try std.testing.expectEqual(
5956 @as(u8, 99),
5957 (try testDatabasePageByte(&database, 1)).?,
5958 );
5959 try std.testing.expectEqual(@as(usize, 0), database.pager.base.items.len);
5960 try std.testing.expectEqual(
5961 @as(u8, 22),
5962 (try testDatabasePageByte(&database, 2)).?,
5963 );
5964 try std.testing.expectEqual(@as(usize, 0), database.pager.base.items.len);
5965 try std.testing.expectEqual(@as(usize, 1), database.read_cache.count());
5966 }
5967
5968 fn modelPathStorageCapacity(limits: PathStorage.Limits) ?PathStorage.Capacity {
5969 var storage_bytes = std.math.add(
5970 usize,
5971 limits.database_bytes,
5972 limits.wal_bytes,
5973 ) catch return null;
5974 storage_bytes = std.math.add(
5975 usize,
5976 storage_bytes,
5977 limits.publication_database_bytes,
5978 ) catch return null;
5979 storage_bytes = std.math.add(
5980 usize,
5981 storage_bytes,
5982 limits.publication_wal_bytes,
5983 ) catch return null;
5984 return .{
5985 .database_bytes = limits.database_bytes,
5986 .wal_bytes = limits.wal_bytes,
5987 .publication_database_bytes = limits.publication_database_bytes,
5988 .publication_wal_bytes = limits.publication_wal_bytes,
5989 .storage_bytes = storage_bytes,
5990 };
5991 }
5992
5993 test "file path storage capacity matches an independent byte-sum model" {
5994 comptime {
5995 @stardustClaim(
5996 @import("alloc_phase").capacity.witness(PathStorage, "sql_path_storage_capacity"),
5997 null,
5998 null,
5999 null,
6000 null,
6001 null,
6002 null,
6003 );
6004 }
6005
6006 for (0..257) |database_bytes| {
6007 const limits: PathStorage.Limits = .{
6008 .database_bytes = database_bytes,
6009 .wal_bytes = database_bytes * 2,
6010 .publication_database_bytes = database_bytes * 3,
6011 .publication_wal_bytes = database_bytes * 4,
6012 };
6013 try std.testing.expectEqual(
6014 modelPathStorageCapacity(limits).?,
6015 try PathStorage.Capacity.derive(limits),
6016 );
6017 }
6018
6019 const maximum: PathStorage.Limits = .{
6020 .database_bytes = std.math.maxInt(usize),
6021 .wal_bytes = 0,
6022 };
6023 try std.testing.expectEqual(
6024 modelPathStorageCapacity(maximum).?,
6025 try PathStorage.Capacity.derive(maximum),
6026 );
6027 const overflow = PathStorage.Limits{
6028 .database_bytes = std.math.maxInt(usize),
6029 .wal_bytes = 1,
6030 };
6031 try std.testing.expect(modelPathStorageCapacity(overflow) == null);
6032 try std.testing.expectError(
6033 error.CapacityOverflow,
6034 PathStorage.Capacity.derive(overflow),
6035 );
6036
6037 var exhausted_identities = PathStorageIdentitySequence{
6038 .next = .init(std.math.maxInt(u64) - 1),
6039 };
6040 try std.testing.expectEqual(
6041 std.math.maxInt(u64),
6042 try exhausted_identities.take(),
6043 );
6044 try std.testing.expectError(
6045 error.PathStorageIdentityExhausted,
6046 exhausted_identities.take(),
6047 );
6048 }
6049
6050 test "file path storage rejects short storage and returns its exact region" {
6051 comptime {
6052 @stardustClaim(
6053 @import("alloc_phase").capacity.witness(PathStorage, "sql_path_storage_rejection"),
6054 null,
6055 null,
6056 null,
6057 null,
6058 null,
6059 null,
6060 );
6061 }
6062
6063 const limits: PathStorage.Limits = .{
6064 .database_bytes = 3,
6065 .wal_bytes = 4,
6066 .publication_database_bytes = 5,
6067 .publication_wal_bytes = 6,
6068 };
6069 const capacity = try PathStorage.Capacity.derive(limits);
6070 var storage: [18]u8 = undefined;
6071 try std.testing.expectEqual(storage.len, capacity.storage_bytes);
6072 try std.testing.expectError(
6073 error.StorageTooShort,
6074 PathStorage.init(storage[0 .. storage.len - 1], limits),
6075 );
6076
6077 var paths = try PathStorage.init(&storage, limits);
6078 try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, paths.phase);
6079 paths.activate();
6080 try std.testing.expectEqual(alloc_phase.capacity.Phase.steady, paths.phase);
6081 const returned = paths.deinit();
6082 try std.testing.expectEqual(storage[0..].ptr, returned.ptr);
6083 try std.testing.expectEqual(storage.len, returned.len);
6084 }
6085
6086 test "file path storage rejects overload before changing any region" {
6087 comptime {
6088 @stardustClaim(
6089 @import("alloc_phase").capacity.witness(PathStorage, "sql_path_storage_overload"),
6090 null,
6091 null,
6092 null,
6093 null,
6094 null,
6095 null,
6096 );
6097 }
6098 comptime {
6099 @stardustClaim(
6100 @import("alloc_phase").capacity.witness(PathStorage, "sql_path_storage_work"),
6101 null,
6102 null,
6103 null,
6104 null,
6105 null,
6106 null,
6107 );
6108 }
6109 comptime {
6110 @stardustClaim(
6111 @import("alloc_phase").capacity.witness(PathStorage, "sql_path_storage_foreign"),
6112 null,
6113 null,
6114 null,
6115 null,
6116 null,
6117 null,
6118 );
6119 }
6120
6121 const limits: PathStorage.Limits = .{
6122 .database_bytes = 3,
6123 .wal_bytes = 4,
6124 .publication_database_bytes = 5,
6125 .publication_wal_bytes = 6,
6126 };
6127 var storage: [18]u8 = undefined;
6128 var paths = try PathStorage.init(&storage, limits);
6129 paths.activate();
6130 var loan = try paths.acquire(limits);
6131 defer {
6132 paths.release(&loan) catch @panic("invalid test path-storage loan");
6133 _ = paths.deinit();
6134 }
6135 const current = try loan.storeCurrent(.{ .database = "db", .wal = "wal" });
6136 const publication_paths = try loan.storePublication(.{
6137 .database = "base",
6138 .wal = "journa",
6139 });
6140 const before = storage;
6141 try std.testing.expectError(
6142 error.PathCapacityExceeded,
6143 loan.storeCurrent(.{ .database = "four", .wal = "wal" }),
6144 );
6145 try std.testing.expectError(
6146 error.PathCapacityExceeded,
6147 loan.storePublication(.{ .database = "base", .wal = "too-long" }),
6148 );
6149 try std.testing.expectEqualSlices(u8, &before, &storage);
6150 try std.testing.expectEqualStrings("db", current.database);
6151 try std.testing.expectEqualStrings("wal", current.wal);
6152 try std.testing.expectEqualStrings("base", publication_paths.database);
6153 try std.testing.expectEqualStrings("journa", publication_paths.wal);
6154 try std.testing.expect(storage.len <= PathStorage.work_limits.transition_steps_max);
6155 }
6156
6157 test "file path storage authenticates copied loans across reacquisition" {
6158 const limits: PathStorage.Limits = .{
6159 .database_bytes = 4,
6160 .wal_bytes = 4,
6161 };
6162 const narrow_limits: PathStorage.Limits = .{
6163 .database_bytes = 2,
6164 .wal_bytes = 3,
6165 };
6166 var storage: [8]u8 = @splat(0xa5);
6167 var paths = try PathStorage.init(&storage, limits);
6168 paths.activate();
6169
6170 var first = try paths.acquire(narrow_limits);
6171 const before_narrow_rejection = storage;
6172 try std.testing.expectError(
6173 error.PathCapacityExceeded,
6174 first.storeCurrent(.{ .database = "wide", .wal = "wal" }),
6175 );
6176 try std.testing.expectEqualSlices(u8, &before_narrow_rejection, &storage);
6177 var stale = first;
6178 try paths.release(&first);
6179
6180 var copied_owner = paths;
6181 try std.testing.expectError(
6182 error.InvalidPathStorageOwner,
6183 copied_owner.acquire(narrow_limits),
6184 );
6185
6186 var second = try paths.acquire(narrow_limits);
6187 try std.testing.expectError(
6188 error.InvalidPathStorageLoan,
6189 stale.storeCurrent(.{ .database = "old", .wal = "old" }),
6190 );
6191 try std.testing.expect(paths.ownsLoan(&second));
6192 try std.testing.expectError(
6193 error.InvalidPathStorageLoan,
6194 paths.release(&stale),
6195 );
6196 try std.testing.expect(paths.ownsLoan(&second));
6197 _ = try second.storeCurrent(.{ .database = "ok", .wal = "wal" });
6198 try paths.release(&second);
6199
6200 const returned = paths.deinit();
6201 paths = try PathStorage.init(returned, limits);
6202 paths.activate();
6203 var third = try paths.acquire(narrow_limits);
6204 try std.testing.expectError(
6205 error.InvalidPathStorageLoan,
6206 stale.storeCurrent(.{ .database = "old", .wal = "old" }),
6207 );
6208 try paths.release(&third);
6209
6210 const before = storage;
6211 paths.loan_epoch = std.math.maxInt(u64);
6212 try std.testing.expectError(
6213 error.PathLoanGenerationExhausted,
6214 paths.acquire(narrow_limits),
6215 );
6216 try std.testing.expect(!paths.loan_live);
6217 try std.testing.expectEqualSlices(u8, &before, &storage);
6218 _ = paths.deinit();
6219 }
6220
6221 test "file path storage remains allocation free after sealing" {
6222 comptime {
6223 @stardustClaim(
6224 @import("alloc_phase").capacity.witness(PathStorage, "sql_path_storage_transitive"),
6225 null,
6226 null,
6227 null,
6228 null,
6229 null,
6230 null,
6231 );
6232 }
6233
6234 const limits: PathStorage.Limits = .{
6235 .database_bytes = 8,
6236 .wal_bytes = 8,
6237 };
6238 const capacity = try PathStorage.Capacity.derive(limits);
6239 var phase_allocator = try alloc_phase.SealedPhaseAllocator.init(std.testing.allocator);
6240 const storage = phase_allocator.initializationAllocator().alloc(
6241 u8,
6242 capacity.storage_bytes,
6243 ) catch |err| {
6244 phase_allocator.abortInitialization();
6245 phase_allocator.deinit();
6246 return err;
6247 };
6248 var paths = PathStorage.init(storage, limits) catch |err| {
6249 phase_allocator.initializationAllocator().free(storage);
6250 phase_allocator.abortInitialization();
6251 phase_allocator.deinit();
6252 return err;
6253 };
6254 var loan: PathStorage.Loan = undefined;
6255 var loan_live = false;
6256 defer {
6257 if (phase_allocator.phase() == .initialization) phase_allocator.abortInitialization();
6258 if (phase_allocator.phase() == .steady) phase_allocator.beginTeardown();
6259 if (loan_live) {
6260 paths.release(&loan) catch @panic("invalid test path-storage loan");
6261 }
6262 const returned = paths.deinit();
6263 phase_allocator.teardownAllocator().free(returned);
6264 phase_allocator.deinit();
6265 }
6266 phase_allocator.seal();
6267 paths.activate();
6268 loan = try paths.acquire(limits);
6269 loan_live = true;
6270 const current = try loan.storeCurrent(.{ .database = "next.db", .wal = "next.wal" });
6271 try std.testing.expectEqualStrings("next.db", current.database);
6272 try std.testing.expectEqualStrings("next.wal", current.wal);
6273 }
6274
6275 fn modelReadCacheCapacity(pages: usize) ?ReadCache.Capacity {
6276 const key_bytes = std.math.mul(usize, pages, @sizeOf(ReadCacheKey)) catch return null;
6277 const image_bytes = std.math.mul(usize, pages, @sizeOf(ReadCacheImage)) catch return null;
6278 const storage_bytes = std.math.add(usize, key_bytes, image_bytes) catch return null;
6279 return .{
6280 .pages = pages,
6281 .storage_bytes = storage_bytes,
6282 };
6283 }
6284
6285 test "file read cache capacity matches an independent typed-byte model" {
6286 comptime {
6287 @stardustClaim(
6288 @import("alloc_phase").capacity.witness(ReadCache, "sql_read_cache_capacity"),
6289 null,
6290 null,
6291 null,
6292 null,
6293 null,
6294 null,
6295 );
6296 }
6297
6298 for (0..4097) |pages| {
6299 try std.testing.expectEqual(
6300 modelReadCacheCapacity(pages).?,
6301 try ReadCache.Capacity.derive(.{ .pages = pages }),
6302 );
6303 }
6304
6305 const maximum_pages = std.math.maxInt(usize) /
6306 (@sizeOf(ReadCacheKey) + @sizeOf(ReadCacheImage));
6307 try std.testing.expectEqual(
6308 modelReadCacheCapacity(maximum_pages).?,
6309 try ReadCache.Capacity.derive(.{ .pages = maximum_pages }),
6310 );
6311 const overflow_pages = maximum_pages + 1;
6312 try std.testing.expect(modelReadCacheCapacity(overflow_pages) == null);
6313 try std.testing.expectError(
6314 error.CapacityOverflow,
6315 ReadCache.Capacity.derive(.{ .pages = overflow_pages }),
6316 );
6317 try std.testing.expectError(
6318 error.CapacityOverflow,
6319 ReadCache.init(
6320 @as(ReadCache.Storage, &.{}),
6321 .{ .pages = overflow_pages },
6322 ),
6323 );
6324 }
6325
6326 test "file read cache rejects short storage and returns its exact region" {
6327 comptime {
6328 @stardustClaim(
6329 @import("alloc_phase").capacity.witness(ReadCache, "sql_read_cache_storage_rejection"),
6330 null,
6331 null,
6332 null,
6333 null,
6334 null,
6335 null,
6336 );
6337 }
6338
6339 const limits: ReadCache.Limits = .{ .pages = 3 };
6340 const capacity = try ReadCache.Capacity.derive(limits);
6341 var storage: [3 * (@sizeOf(ReadCacheKey) + @sizeOf(ReadCacheImage))]u8 align(ReadCache.storage_alignment) = undefined;
6342 try std.testing.expectEqual(storage.len, capacity.storage_bytes);
6343 try std.testing.expectError(
6344 error.StorageTooShort,
6345 ReadCache.init(storage[0 .. storage.len - 1], limits),
6346 );
6347
6348 var cache = try ReadCache.init(&storage, limits);
6349 try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, cache.phase);
6350 cache.activate();
6351 try std.testing.expectEqual(alloc_phase.capacity.Phase.steady, cache.phase);
6352 const returned = cache.deinit();
6353 try std.testing.expectEqual(storage[0..].ptr, returned.ptr);
6354 try std.testing.expectEqual(storage.len, returned.len);
6355 }
6356
6357 test "file read cache bounds linear passes by provisioned capacity" {
6358 comptime {
6359 @stardustClaim(
6360 @import("alloc_phase").capacity.witness(ReadCache, "sql_read_cache_work_bound"),
6361 null,
6362 null,
6363 null,
6364 null,
6365 null,
6366 null,
6367 );
6368 }
6369
6370 const pages = 17;
6371 var storage: [pages * (@sizeOf(ReadCacheKey) + @sizeOf(ReadCacheImage))]u8 align(ReadCache.storage_alignment) = undefined;
6372 var cache = try ReadCache.init(&storage, .{ .pages = pages });
6373 defer _ = cache.deinit();
6374 cache.activate();
6375 var image: [page.size]u8 = undefined;
6376 @memset(&image, 31);
6377 for (1..pages + 1) |page_id| {
6378 try cache.put(.{ .generation = 1, .page_id = @intCast(page_id) }, &image);
6379 }
6380 try std.testing.expectEqual(pages, cache.count());
6381 cache.clearRetainingCapacity();
6382 try std.testing.expectEqual(@as(usize, 0), cache.count());
6383 try std.testing.expect(pages <= ReadCache.work_limits.transition_steps_max);
6384 }
6385
6386 test "file read cache is sealed before replacement and clear" {
6387 comptime {
6388 @stardustClaim(
6389 @import("alloc_phase").capacity.witness(ReadCache, "sql_read_cache_sealed_overload"),
6390 null,
6391 null,
6392 null,
6393 null,
6394 null,
6395 null,
6396 );
6397 }
6398 comptime {
6399 @stardustClaim(
6400 @import("alloc_phase").capacity.witness(ReadCache, "sql_read_cache_sealed_transitive_risk"),
6401 null,
6402 null,
6403 null,
6404 null,
6405 null,
6406 null,
6407 );
6408 }
6409
6410 var disabled = try ReadCache.init(
6411 @as(ReadCache.Storage, &.{}),
6412 .{ .pages = 0 },
6413 );
6414 defer _ = disabled.deinit();
6415 disabled.activate();
6416 var disabled_image: [page.size]u8 = undefined;
6417 @memset(&disabled_image, 0);
6418 try std.testing.expectError(
6419 error.CacheDisabled,
6420 disabled.put(.{ .generation = 1, .page_id = 1 }, &disabled_image),
6421 );
6422 try std.testing.expectEqual(@as(usize, 0), disabled.count());
6423
6424 var phase_allocator = try alloc_phase.SealedPhaseAllocator.init(std.testing.allocator);
6425 const limits: ReadCache.Limits = .{ .pages = 2 };
6426 const derived_capacity = try ReadCache.Capacity.derive(limits);
6427 const storage = phase_allocator.initializationAllocator().alignedAlloc(
6428 u8,
6429 .fromByteUnits(ReadCache.storage_alignment),
6430 derived_capacity.storage_bytes,
6431 ) catch |err| {
6432 phase_allocator.abortInitialization();
6433 phase_allocator.deinit();
6434 return err;
6435 };
6436 var cache = ReadCache.init(
6437 storage,
6438 limits,
6439 ) catch |err| {
6440 phase_allocator.initializationAllocator().free(storage);
6441 phase_allocator.abortInitialization();
6442 phase_allocator.deinit();
6443 return err;
6444 };
6445 defer {
6446 if (phase_allocator.phase() == .initialization) phase_allocator.abortInitialization();
6447 if (phase_allocator.phase() == .steady) phase_allocator.beginTeardown();
6448 const returned = cache.deinit();
6449 phase_allocator.teardownAllocator().free(returned);
6450 phase_allocator.deinit();
6451 }
6452
6453 const keys_pointer = cache.keys.ptr;
6454 const capacity = cache.capacity;
6455 phase_allocator.seal();
6456 cache.activate();
6457
6458 var first: [page.size]u8 = undefined;
6459 var replacement: [page.size]u8 = undefined;
6460 var copied: [page.size]u8 = undefined;
6461 @memset(&first, 11);
6462 @memset(&replacement, 33);
6463 try cache.put(.{ .generation = 1, .page_id = 1 }, &first);
6464 try std.testing.expect(cache.get(.{ .generation = 1, .page_id = 1 }, &copied));
6465 try std.testing.expectEqualSlices(u8, &first, &copied);
6466
6467 try cache.put(.{ .generation = 1, .page_id = 3 }, &replacement);
6468 try std.testing.expect(!cache.get(.{ .generation = 1, .page_id = 1 }, &copied));
6469 try std.testing.expect(cache.get(.{ .generation = 1, .page_id = 3 }, &copied));
6470 try std.testing.expectEqualSlices(u8, &replacement, &copied);
6471 try std.testing.expectEqual(@as(usize, 1), cache.count());
6472 try std.testing.expectEqual(keys_pointer, cache.keys.ptr);
6473 try std.testing.expectEqual(capacity, cache.capacity);
6474
6475 cache.clearRetainingCapacity();
6476 try std.testing.expectEqual(@as(usize, 0), cache.count());
6477 try std.testing.expectEqual(@as(u64, 0), phase_allocator.violations().total());
6478 }
6479
6480 fn modelTransactionStagingCapacity(frames: usize) ?TransactionStaging.Capacity {
6481 if (frames > std.math.maxInt(u32)) return null;
6482 if (frames > std.math.maxInt(usize) / 2) return null;
6483 const index_slots = frames * 2;
6484 if (index_slots > std.math.maxInt(usize) / @sizeOf(TransactionIndexEntry)) return null;
6485 return .{
6486 .frames = frames,
6487 .index_slots = index_slots,
6488 .storage_bytes = index_slots * @sizeOf(TransactionIndexEntry),
6489 };
6490 }
6491
6492 test "file transaction staging capacity matches an independent dense index model" {
6493 comptime {
6494 @stardustClaim(
6495 @import("alloc_phase").capacity.witness(TransactionStaging, "sql_file_transaction_staging_capacity"),
6496 null,
6497 null,
6498 null,
6499 null,
6500 null,
6501 null,
6502 );
6503 }
6504
6505 for (0..4097) |frames| {
6506 try std.testing.expectEqual(
6507 modelTransactionStagingCapacity(frames).?,
6508 try TransactionStaging.Capacity.derive(.{ .frames = frames }),
6509 );
6510 }
6511
6512 const maximum_frames = @min(std.math.maxInt(u32), std.math.maxInt(usize) / (2 * @sizeOf(TransactionIndexEntry)));
6513 try std.testing.expectEqual(
6514 modelTransactionStagingCapacity(maximum_frames).?,
6515 try TransactionStaging.Capacity.derive(.{ .frames = maximum_frames }),
6516 );
6517 const overflow_frames = maximum_frames + 1;
6518 try std.testing.expect(modelTransactionStagingCapacity(overflow_frames) == null);
6519 try std.testing.expectError(
6520 error.CapacityOverflow,
6521 TransactionStaging.Capacity.derive(.{ .frames = overflow_frames }),
6522 );
6523 try std.testing.expectError(
6524 error.CapacityOverflow,
6525 TransactionStaging.init(
6526 @as(TransactionStaging.Storage, &.{}),
6527 .{ .frames = overflow_frames },
6528 ),
6529 );
6530 }
6531
6532 test "file transaction staging rejects short storage and returns its exact region" {
6533 const limits: TransactionStaging.Limits = .{ .frames = 17 };
6534 const capacity = try TransactionStaging.Capacity.derive(limits);
6535 const storage = try std.testing.allocator.alignedAlloc(
6536 u8,
6537 .fromByteUnits(TransactionStaging.storage_alignment),
6538 capacity.storage_bytes,
6539 );
6540 defer std.testing.allocator.free(storage);
6541 try std.testing.expectError(
6542 error.StorageTooShort,
6543 TransactionStaging.init(storage[0 .. storage.len - 1], limits),
6544 );
6545
6546 var staging = try TransactionStaging.init(storage, limits);
6547 try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, staging.phase);
6548 staging.activate();
6549 try std.testing.expectEqual(alloc_phase.capacity.Phase.steady, staging.phase);
6550 const returned = staging.deinit();
6551 try std.testing.expectEqual(storage.ptr, returned.ptr);
6552 try std.testing.expectEqual(storage.len, returned.len);
6553 }
6554
6555 test "file transaction staging bounds generation-wrap cleanup by provisioned capacity" {
6556 comptime {
6557 @stardustClaim(
6558 @import("alloc_phase").capacity.witness(TransactionStaging, "sql_file_transaction_staging_work_bound"),
6559 null,
6560 null,
6561 null,
6562 null,
6563 null,
6564 null,
6565 );
6566 }
6567
6568 const frames = 17;
6569 var storage: [frames * 2 * @sizeOf(TransactionIndexEntry)]u8 align(TransactionStaging.storage_alignment) = undefined;
6570 var staging = try TransactionStaging.init(&storage, .{ .frames = frames });
6571 defer _ = staging.deinit();
6572 staging.activate();
6573 for (staging.index, 0..) |*entry, index| {
6574 entry.* = .{
6575 .generation = std.math.maxInt(u32),
6576 .page_index = @intCast(index),
6577 };
6578 }
6579 staging.generation = std.math.maxInt(u32);
6580
6581 staging.begin(frames);
6582 try std.testing.expectEqual(@as(u32, 1), staging.generation);
6583 for (staging.index) |entry| try std.testing.expectEqual(TransactionIndexEntry{}, entry);
6584 staging.end();
6585 }
6586
6587 test "file transaction staging is sealed through maximum occupancy and overload" {
6588 comptime {
6589 @stardustClaim(
6590 @import("alloc_phase").capacity.witness(TransactionStaging, "sql_file_transaction_staging_sealed"),
6591 null,
6592 null,
6593 null,
6594 null,
6595 null,
6596 null,
6597 );
6598 }
6599
6600 var empty = try TransactionStaging.init(
6601 @as(TransactionStaging.Storage, &.{}),
6602 .{ .frames = 0 },
6603 );
6604 empty.activate();
6605 empty.begin(0);
6606 try std.testing.expectError(error.TransactionTooLarge, empty.nextPage());
6607 empty.end();
6608 _ = empty.deinit();
6609
6610 var phase_allocator = try alloc_phase.SealedPhaseAllocator.init(std.testing.allocator);
6611 const limits: TransactionStaging.Limits = .{ .frames = 17 };
6612 const derived_capacity = try TransactionStaging.Capacity.derive(limits);
6613 const storage = phase_allocator.initializationAllocator().alignedAlloc(
6614 u8,
6615 .fromByteUnits(TransactionStaging.storage_alignment),
6616 derived_capacity.storage_bytes,
6617 ) catch |err| {
6618 phase_allocator.abortInitialization();
6619 phase_allocator.deinit();
6620 return err;
6621 };
6622 var staging = TransactionStaging.init(storage, limits) catch |err| {
6623 phase_allocator.initializationAllocator().free(storage);
6624 phase_allocator.abortInitialization();
6625 phase_allocator.deinit();
6626 return err;
6627 };
6628 defer {
6629 if (phase_allocator.phase() == .initialization) phase_allocator.abortInitialization();
6630 if (phase_allocator.phase() == .steady) phase_allocator.beginTeardown();
6631 const returned = staging.deinit();
6632 phase_allocator.teardownAllocator().free(returned);
6633 phase_allocator.deinit();
6634 }
6635
6636 const index_pointer = staging.index.ptr;
6637 const capacity = staging.capacity;
6638 phase_allocator.seal();
6639 staging.activate();
6640 staging.begin(capacity.frames);
6641
6642 for (0..capacity.frames) |page_index| {
6643 try std.testing.expectEqual(page_index, try staging.nextPage());
6644 staging.occupy(page_index * 2, page_index);
6645 try std.testing.expectEqual(page_index, staging.pageIndex(page_index * 2).?);
6646 try std.testing.expect(staging.pageIndex(page_index * 2 + 1) == null);
6647 }
6648 try std.testing.expectEqual(capacity.frames, staging.pages);
6649
6650 var index_before: [34]TransactionIndexEntry = undefined;
6651 @memcpy(&index_before, staging.index);
6652 try std.testing.expectError(error.TransactionTooLarge, staging.nextPage());
6653 try std.testing.expectEqual(capacity.frames, staging.pages);
6654 try std.testing.expectEqualSlices(TransactionIndexEntry, &index_before, staging.index);
6655 try std.testing.expectEqual(index_pointer, staging.index.ptr);
6656 try std.testing.expectEqual(capacity, staging.capacity);
6657
6658 const first_generation = staging.generation;
6659 staging.end();
6660 staging.begin(3);
6661 try std.testing.expectEqual(first_generation + 1, staging.generation);
6662 for (0..capacity.index_slots) |index| try std.testing.expect(staging.pageIndex(index) == null);
6663 staging.occupy(1, try staging.nextPage());
6664 try std.testing.expectEqual(@as(usize, 0), staging.pageIndex(1).?);
6665 staging.end();
6666
6667 staging.generation = std.math.maxInt(u32);
6668 staging.begin(1);
6669 try std.testing.expectEqual(@as(u32, 1), staging.generation);
6670 for (0..capacity.index_slots) |index| try std.testing.expect(staging.pageIndex(index) == null);
6671 staging.end();
6672 try std.testing.expectEqual(@as(u64, 0), phase_allocator.violations().total());
6673 }
6674
6675 test "file snapshot copyPage caches base reads without pager retention" {
6676 var tmp = std.testing.tmpDir(.{});
6677 defer tmp.cleanup();
6678
6679 var source = try testingPager(0);
6680 defer deinitTestingPager(&source);
6681
6682 const total_pages: u32 = @intCast(default_read_cache_capacity + 9);
6683 var page_id: u32 = 1;
6684 while (page_id <= total_pages) : (page_id += 1) {
6685 var image: [page.size]u8 = undefined;
6686 @memset(&image, 0);
6687 image[0] = @truncate(page_id);
6688 image[1] = @intCast(page_id % 251);
6689 try source.installBase(page_id, &image);
6690 }
6691 try writeBase(testing_io, tmp.dir, "main.db", &source);
6692 try writeWal(testing_io, tmp.dir, "main.wal", &source);
6693
6694 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
6695 .paths = .{ .database = "main.db", .wal = "main.wal" },
6696 .header = testingHeader(),
6697 });
6698 defer database.deinit();
6699
6700 try std.testing.expectEqual(@as(usize, 0), database.pager.base.items.len);
6701 try std.testing.expectEqual(@as(usize, 0), database.read_cache.count());
6702
6703 var read = try database.beginRead();
6704 defer read.deinit();
6705 const snapshot = read.snapshot();
6706 var image: [page.size]u8 = undefined;
6707 try std.testing.expect(try snapshot.copyPage(7, &image));
6708 try std.testing.expectEqual(@as(u8, 7), image[1]);
6709 try std.testing.expectEqual(@as(usize, 0), database.pager.base.items.len);
6710 try std.testing.expectEqual(@as(usize, 1), database.read_cache.count());
6711
6712 try std.testing.expect(try snapshot.copyPage(7, &image));
6713 try std.testing.expectEqual(@as(usize, 1), database.read_cache.count());
6714
6715 page_id = 1;
6716 while (page_id <= total_pages) : (page_id += 1) try std.testing.expect(try snapshot.copyPage(page_id, &image));
6717 try std.testing.expectEqual(@as(usize, 0), database.pager.base.items.len);
6718 try std.testing.expectEqual(@as(usize, default_read_cache_capacity), database.read_cache.count());
6719 }
6720
6721 test "file snapshot copies carry the check marks of their stored images" {
6722 var tmp = std.testing.tmpDir(.{});
6723 defer tmp.cleanup();
6724
6725 var source = try testingPager(0);
6726 defer deinitTestingPager(&source);
6727
6728 const capacity: u32 = @intCast(default_read_cache_capacity);
6729 var page_id: u32 = 1;
6730 while (page_id <= capacity + 7) : (page_id += 1) {
6731 var image: [page.size]u8 = undefined;
6732 fillImage(&image, page_id, @intCast(page_id % 251));
6733 try source.installBase(page_id, &image);
6734 }
6735 try writeBase(testing_io, tmp.dir, "main.db", &source);
6736 try writeWal(testing_io, tmp.dir, "main.wal", &source);
6737
6738 var image: [page.size]u8 = undefined;
6739 {
6740 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
6741 .paths = .{ .database = "main.db", .wal = "main.wal" },
6742 .header = testingHeader(),
6743 });
6744 defer database.deinit();
6745
6746 {
6747 var read = try database.beginRead();
6748 defer read.deinit();
6749 const snapshot = read.snapshot();
6750 const cached = (try snapshot.copyMarkedPage(7, &image)).?;
6751 try std.testing.expect(!cached.checked());
6752 cached.record();
6753 try std.testing.expect((try snapshot.copyMarkedPage(7, &image)).?.checked());
6754 const evicting = (try snapshot.copyMarkedPage(capacity + 7, &image)).?;
6755 try std.testing.expect(!evicting.checked());
6756 try std.testing.expect(!(try snapshot.copyMarkedPage(7, &image)).?.checked());
6757 try std.testing.expectEqual(@as(u8, 7), image[1]);
6758 }
6759
6760 const point = try database.savepoint();
6761 for ([_]u8{ 70, 71 }) |value| {
6762 var transaction = try database.beginWrite();
6763 defer transaction.deinit();
6764 fillImage(&image, 7, value);
6765 try transaction.putPage(7, &image);
6766 _ = try transaction.commit(.{ .durability = .buffered });
6767
6768 {
6769 var read = try database.beginRead();
6770 defer read.deinit();
6771 const logged = (try read.snapshot().copyMarkedPage(7, &image)).?;
6772 try std.testing.expectEqual(value, image[1]);
6773 try std.testing.expect(!logged.checked());
6774 logged.record();
6775 try std.testing.expect((try read.snapshot().copyMarkedPage(7, &image)).?.checked());
6776 }
6777 try database.restore(point);
6778 }
6779 }
6780
6781 var disabled = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
6782 .paths = .{ .database = "main.db", .wal = "main.wal" },
6783 .header = testingHeader(),
6784 .read_cache_capacity = 0,
6785 });
6786 defer disabled.deinit();
6787 var read = try disabled.beginRead();
6788 defer read.deinit();
6789 const uncached = (try read.snapshot().copyMarkedPage(7, &image)).?;
6790 uncached.record();
6791 try std.testing.expect(!(try read.snapshot().copyMarkedPage(7, &image)).?.checked());
6792 }
6793
6794 test "file snapshot copyPage honors read cache capacity option" {
6795 comptime {
6796 @stardustClaim(
6797 @import("alloc_phase").capacity.witness(ReadCache, "sql_read_cache_semantics_overload"),
6798 null,
6799 null,
6800 null,
6801 null,
6802 null,
6803 null,
6804 );
6805 }
6806 comptime {
6807 @stardustClaim(
6808 @import("alloc_phase").capacity.witness(ReadCache, "sql_read_cache_semantics_foreign_risk"),
6809 null,
6810 null,
6811 null,
6812 null,
6813 null,
6814 null,
6815 );
6816 }
6817
6818 var tmp = std.testing.tmpDir(.{});
6819 defer tmp.cleanup();
6820
6821 var source = try testingPager(0);
6822 defer deinitTestingPager(&source);
6823
6824 var page_id: u32 = 1;
6825 while (page_id <= 9) : (page_id += 1) {
6826 var image: [page.size]u8 = undefined;
6827 @memset(&image, 0);
6828 image[0] = @truncate(page_id);
6829 image[1] = @intCast(page_id % 251);
6830 try source.installBase(page_id, &image);
6831 }
6832 try writeBase(testing_io, tmp.dir, "main.db", &source);
6833 try writeWal(testing_io, tmp.dir, "main.wal", &source);
6834
6835 {
6836 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
6837 .paths = .{ .database = "main.db", .wal = "main.wal" },
6838 .header = testingHeader(),
6839 .read_cache_capacity = 4,
6840 });
6841 defer database.deinit();
6842
6843 var read = try database.beginRead();
6844 defer read.deinit();
6845 const snapshot = read.snapshot();
6846 var image: [page.size]u8 = undefined;
6847 page_id = 1;
6848 while (page_id <= 9) : (page_id += 1) try std.testing.expect(try snapshot.copyPage(page_id, &image));
6849 try std.testing.expectEqual(@as(usize, 4), database.read_cache.keys.len);
6850 try std.testing.expectEqual(@as(usize, 4), database.read_cache.count());
6851 }
6852
6853 var disabled = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
6854 .paths = .{ .database = "main.db", .wal = "main.wal" },
6855 .header = testingHeader(),
6856 .read_cache_capacity = 0,
6857 });
6858 defer disabled.deinit();
6859 var read = try disabled.beginRead();
6860 defer read.deinit();
6861 const snapshot = read.snapshot();
6862 var image: [page.size]u8 = undefined;
6863 try std.testing.expect(try snapshot.copyPage(7, &image));
6864 try std.testing.expectEqual(@as(u8, 7), image[1]);
6865 try std.testing.expectEqual(@as(usize, 0), disabled.read_cache.keys.len);
6866 try std.testing.expectEqual(@as(usize, 0), disabled.read_cache.count());
6867 }
6868
6869 test "file checkpoints stream prepared WAL pages before restart" {
6870 comptime {
6871 @stardustClaim(
6872 @import("alloc_phase").capacity.witness(ReadCache, "sql_read_cache_checkpoint_semantics"),
6873 null,
6874 null,
6875 null,
6876 null,
6877 null,
6878 null,
6879 );
6880 }
6881 comptime {
6882 @stardustClaim(
6883 @import("alloc_phase").capacity.witness(@import("./root.zig").CheckpointPlan, "sql_checkpoint_plan_semantics_transitive_risk"),
6884 null,
6885 null,
6886 null,
6887 null,
6888 null,
6889 null,
6890 );
6891 }
6892 comptime {
6893 @stardustClaim(
6894 @import("alloc_phase").capacity.witness(@import("./root.zig").CheckpointPlan, "sql_checkpoint_plan_semantics_foreign_risk"),
6895 null,
6896 null,
6897 null,
6898 null,
6899 null,
6900 null,
6901 );
6902 }
6903
6904 var tmp = std.testing.tmpDir(.{});
6905 defer tmp.cleanup();
6906 var debug_allocator: std.heap.DebugAllocator(.{ .enable_memory_limit = true }) = .init;
6907 defer _ = debug_allocator.deinit();
6908 const allocator = debug_allocator.allocator();
6909
6910 {
6911 var database = try Database.openForTesting(allocator, tmp.dir, .{
6912 .paths = .{ .database = "main.db", .wal = "main.wal" },
6913 .header = testingHeader(),
6914 .read_cache_capacity = 2,
6915 });
6916 defer database.deinit();
6917 try database.reserve(.{ .wal_frames = 3, .wal_pages = 3 });
6918
6919 var checkpoint_live_bytes: ?usize = null;
6920 for (0..3) |cycle| {
6921 for (0..3) |index| {
6922 const page_id: u32 = @intCast(cycle * 3 + index + 1);
6923 var image: [page.size]u8 = undefined;
6924 fillImage(&image, page_id, @intCast(100 + page_id));
6925 try database.appendWal(
6926 page_id,
6927 if (index == 2) page_id else 0,
6928 &image,
6929 );
6930 }
6931 try database.syncWal();
6932 const checkpoint_value = try database.checkpoint(.{ .restart_header = recoveredHeader() });
6933 try std.testing.expect(checkpoint_value.restarted);
6934 try std.testing.expectEqual(@as(usize, 3), checkpoint_value.pages);
6935 try std.testing.expectEqual(@as(usize, 0), database.pager.storage().base_images);
6936 try std.testing.expectEqual(@as(usize, 0), database.pager.storage().base_capacity);
6937 if (checkpoint_live_bytes) |expected| {
6938 try std.testing.expectEqual(expected, debug_allocator.total_requested_bytes);
6939 } else {
6940 checkpoint_live_bytes = debug_allocator.total_requested_bytes;
6941 }
6942
6943 var read = try database.beginRead();
6944 defer read.deinit();
6945 const snapshot = read.snapshot();
6946 var copied: [page.size]u8 = undefined;
6947 const last_page: u32 = @intCast((cycle + 1) * 3);
6948 try std.testing.expect(try snapshot.copyPage(last_page, &copied));
6949 try std.testing.expectEqual(@as(u8, @intCast(100 + last_page)), copied[1]);
6950 try std.testing.expect(database.read_cache.count() <= 2);
6951 }
6952
6953 try database.flush();
6954 try std.testing.expect(!database.base_loaded);
6955 try std.testing.expectEqual(@as(usize, 0), database.pager.storage().base_images);
6956 try std.testing.expectEqual(@as(usize, 0), database.pager.storage().base_capacity);
6957 }
6958 try std.testing.expectEqual(@as(usize, 0), debug_allocator.total_requested_bytes);
6959
6960 var reopened = try Database.openForTesting(allocator, tmp.dir, .{
6961 .paths = .{ .database = "main.db", .wal = "main.wal" },
6962 .header = recoveredHeader(),
6963 .read_cache_capacity = 2,
6964 });
6965 defer reopened.deinit();
6966 var snapshot_read = try reopened.beginRead();
6967 defer snapshot_read.deinit();
6968 const snapshot = snapshot_read.snapshot();
6969 for (1..10) |page_index| {
6970 var copied: [page.size]u8 = undefined;
6971 const page_id: u32 = @intCast(page_index);
6972 try std.testing.expect(try snapshot.copyPage(page_id, &copied));
6973 try std.testing.expectEqual(@as(u8, @intCast(100 + page_id)), copied[1]);
6974 }
6975 try std.testing.expect(reopened.read_cache.count() <= 2);
6976 }
6977
6978 test "file recover materializes clean base pages" {
6979 var tmp = std.testing.tmpDir(.{});
6980 defer tmp.cleanup();
6981
6982 var source = try testingPager(0);
6983 defer deinitTestingPager(&source);
6984
6985 var first: [page.size]u8 = undefined;
6986 var second: [page.size]u8 = undefined;
6987 fillImage(&first, 1, 13);
6988 fillImage(&second, 2, 23);
6989
6990 try source.installBase(1, &first);
6991 try source.installBase(2, &second);
6992 try writeBase(testing_io, tmp.dir, "main.db", &source);
6993 try writeWal(testing_io, tmp.dir, "main.wal", &source);
6994
6995 var workspace = try DatabaseWorkspace.allocate(std.testing.allocator, .{
6996 .header = testingHeader(),
6997 .max_wal_bytes = default_max_wal_bytes,
6998 .path_storage = .{ .database_bytes = 0, .wal_bytes = 0 },
6999 });
7000 defer workspace.deallocate(std.testing.allocator);
7001 var recovered = try recover(std.testing.allocator, &workspace, tmp.dir, .{
7002 .paths = .{ .database = "main.db", .wal = "main.wal" },
7003 .header = testingHeader(),
7004 });
7005 defer workspace.release(&recovered);
7006
7007 const snapshot = try recovered.beginRead();
7008 try std.testing.expectEqual(@as(usize, 2), recovered.base.items.len);
7009 try std.testing.expectEqual(@as(u8, 23), (try snapshot.get(2)).?[1]);
7010 }
7011
7012 test "file recovery rewrites committed wal frames" {
7013 var tmp = std.testing.tmpDir(.{});
7014 defer tmp.cleanup();
7015
7016 var source = try testingPager(1);
7017 defer deinitTestingPager(&source);
7018
7019 var base: [page.size]u8 = undefined;
7020 var committed: [page.size]u8 = undefined;
7021 fillImage(&base, 1, 10);
7022 fillImage(&committed, 1, 20);
7023
7024 try source.installBase(1, &base);
7025 try source.appendWal(1, 1, &committed);
7026 try writeBase(testing_io, tmp.dir, "main.db", &source);
7027 try writeWal(testing_io, tmp.dir, "main.wal", &source);
7028
7029 var workspace = try DatabaseWorkspace.allocate(std.testing.allocator, .{
7030 .header = recoveredHeader(),
7031 .max_wal_bytes = default_max_wal_bytes,
7032 .path_storage = .{ .database_bytes = 0, .wal_bytes = 0 },
7033 });
7034 defer workspace.deallocate(std.testing.allocator);
7035 var recovered = try recoverState(std.testing.allocator, &workspace, tmp.dir, .{
7036 .paths = .{ .database = "main.db", .wal = "main.wal" },
7037 .header = recoveredHeader(),
7038 });
7039 defer recovered.deinit();
7040
7041 try std.testing.expect(recovered.rewrite_base);
7042 try std.testing.expect(recovered.rewrite_wal);
7043 const snapshot = try recovered.pager.beginRead();
7044 try std.testing.expectEqual(@as(u8, 20), (try snapshot.get(1)).?[1]);
7045 }
7046
7047 test "file recovery rejects partial database pages" {
7048 var tmp = std.testing.tmpDir(.{});
7049 defer tmp.cleanup();
7050
7051 var file = try tmp.dir.createFile(testing_io, "broken.db", .{ .read = true, .truncate = true });
7052 defer file.close(testing_io);
7053 try file.writePositionalAll(testing_io, "abc", 0);
7054 try file.setLength(testing_io, 3);
7055
7056 var workspace = try DatabaseWorkspace.allocate(std.testing.allocator, .{
7057 .header = recoveredHeader(),
7058 .max_wal_bytes = default_max_wal_bytes,
7059 .path_storage = .{ .database_bytes = 0, .wal_bytes = 0 },
7060 });
7061 defer workspace.deallocate(std.testing.allocator);
7062 try std.testing.expectError(error.InvalidDatabaseFile, recover(std.testing.allocator, &workspace, tmp.dir, .{
7063 .paths = .{ .database = "broken.db", .wal = "missing.wal" },
7064 .header = recoveredHeader(),
7065 }));
7066 }
7067
7068 test "file open removes a stale wal rewrite sidecar" {
7069 var tmp = std.testing.tmpDir(.{});
7070 defer tmp.cleanup();
7071
7072 {
7073 var sidecar = try tmp.dir.createFile(testing_io, "main.wal.next", .{ .read = true, .truncate = true });
7074 defer sidecar.close(testing_io);
7075 try sidecar.writePositionalAll(testing_io, "torn rewrite leftovers", 0);
7076 }
7077
7078 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7079 .paths = .{ .database = "main.db", .wal = "main.wal" },
7080 .header = testingHeader(),
7081 });
7082 defer database.deinit();
7083
7084 try std.testing.expectError(error.FileNotFound, tmp.dir.openFile(testing_io, "main.wal.next", .{}));
7085 }
7086
7087 test "file recovery heals a torn base tail covered by the wal" {
7088 var tmp = std.testing.tmpDir(.{});
7089 defer tmp.cleanup();
7090
7091 var source = try testingPager(1);
7092 defer deinitTestingPager(&source);
7093
7094 var committed: [page.size]u8 = undefined;
7095 fillImage(&committed, 2, 42);
7096 try source.appendWal(2, 2, &committed);
7097 try writeWal(testing_io, tmp.dir, "main.wal", &source);
7098
7099 {
7100 var base = try tmp.dir.createFile(testing_io, "main.db", .{ .read = true, .truncate = true });
7101 defer base.close(testing_io);
7102 const torn = @as([(page.size + 1)]u8, @splat(0));
7103 try base.writePositionalAll(testing_io, torn[0..], 0);
7104 try base.setLength(testing_io, torn.len);
7105 }
7106
7107 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7108 .paths = .{ .database = "main.db", .wal = "main.wal" },
7109 .header = testingHeader(),
7110 });
7111 defer database.deinit();
7112
7113 try std.testing.expectEqual(
7114 @as(u8, 42),
7115 (try testDatabasePageByte(&database, 2)).?,
7116 );
7117 }
7118
7119 test "file recovery rejects a torn base tail the wal does not cover" {
7120 var tmp = std.testing.tmpDir(.{});
7121 defer tmp.cleanup();
7122
7123 var source = try testingPager(1);
7124 defer deinitTestingPager(&source);
7125
7126 var committed: [page.size]u8 = undefined;
7127 fillImage(&committed, 1, 7);
7128 try source.appendWal(1, 1, &committed);
7129 try writeWal(testing_io, tmp.dir, "main.wal", &source);
7130
7131 {
7132 var base = try tmp.dir.createFile(testing_io, "main.db", .{ .read = true, .truncate = true });
7133 defer base.close(testing_io);
7134 const torn = @as([(page.size + 1)]u8, @splat(0));
7135 try base.writePositionalAll(testing_io, torn[0..], 0);
7136 try base.setLength(testing_io, torn.len);
7137 }
7138
7139 try std.testing.expectError(error.InvalidDatabaseFile, Database.openForTesting(std.testing.allocator, tmp.dir, .{
7140 .paths = .{ .database = "main.db", .wal = "main.wal" },
7141 .header = testingHeader(),
7142 }));
7143 }
7144
7145 test "file database appends wal incrementally and recovers after reopen" {
7146 var tmp = std.testing.tmpDir(.{});
7147 defer tmp.cleanup();
7148
7149 {
7150 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7151 .paths = .{ .database = "main.db", .wal = "main.wal" },
7152 .header = testingHeader(),
7153 });
7154 defer database.deinit();
7155
7156 var committed: [page.size]u8 = undefined;
7157 fillImage(&committed, 1, 42);
7158 try database.appendWal(1, 1, &committed);
7159 try std.testing.expectEqual(wal.header_size + wal.frame_size, database.wal_written);
7160 try database.syncWal();
7161 }
7162
7163 var reopened = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7164 .paths = .{ .database = "main.db", .wal = "main.wal" },
7165 .header = recoveredHeader(),
7166 });
7167 defer reopened.deinit();
7168
7169 var snapshot_read = try reopened.beginRead();
7170 defer snapshot_read.deinit();
7171 const snapshot = snapshot_read.snapshot();
7172 try std.testing.expectEqual(@as(usize, 0), snapshot.view.end_mark);
7173 try std.testing.expectEqual(@as(usize, 0), reopened.pager.frameCount());
7174 try std.testing.expectEqual(@as(u8, 42), (try testSnapshotPageByte(snapshot, 1)).?);
7175 try std.testing.expectEqual(wal.header_size, reopened.wal_written);
7176 }
7177
7178 test "file database checkpoint restart persists base and truncates wal" {
7179 var tmp = std.testing.tmpDir(.{});
7180 defer tmp.cleanup();
7181
7182 {
7183 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7184 .paths = .{ .database = "main.db", .wal = "main.wal" },
7185 .header = testingHeader(),
7186 });
7187 defer database.deinit();
7188
7189 var committed: [page.size]u8 = undefined;
7190 fillImage(&committed, 1, 55);
7191 try database.appendWalSync(1, 1, &committed);
7192 const checkpoint = try database.checkpoint(.{ .restart_header = recoveredHeader() });
7193
7194 try std.testing.expect(checkpoint.restarted);
7195 try std.testing.expectEqual(wal.header_size, database.wal_written);
7196 try std.testing.expectEqual(@as(usize, 0), database.pager.frameCount());
7197 }
7198
7199 var reopened = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7200 .paths = .{ .database = "main.db", .wal = "main.wal" },
7201 .header = .{ .sequence = 79, .salt = .{ .first = 0xaa55_aa55, .second = 0x55aa_55aa } },
7202 });
7203 defer reopened.deinit();
7204
7205 var snapshot_read = try reopened.beginRead();
7206 defer snapshot_read.deinit();
7207 const snapshot = snapshot_read.snapshot();
7208 try std.testing.expectEqual(@as(u8, 55), (try testSnapshotPageByte(snapshot, 1)).?);
7209 try std.testing.expectEqual(@as(usize, 0), snapshot.view.end_mark);
7210 }
7211
7212 test "file publication read lease retains wal view across candidate checkpoint" {
7213 var tmp = std.testing.tmpDir(.{});
7214 defer tmp.cleanup();
7215
7216 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7217 .header = testingHeader(),
7218 .publication = .{
7219 .base_paths = .{ .database = "main.db", .wal = "main.wal" },
7220 .selected = null,
7221 .candidate = 1,
7222 },
7223 });
7224 defer database.deinit();
7225
7226 {
7227 var first: [page.size]u8 = undefined;
7228 var second: [page.size]u8 = undefined;
7229 fillImage(&first, 1, 55);
7230 fillImage(&second, 1, 77);
7231 try database.appendWalSync(1, 1, &first);
7232 var oldest_read = try database.beginRead();
7233 defer oldest_read.deinit();
7234 const oldest = oldest_read.snapshot();
7235 try database.appendWalSync(1, 1, &second);
7236 try std.testing.expectError(
7237 error.ActiveReaders,
7238 database.checkpoint(.{ .restart_header = recoveredHeader() }),
7239 );
7240 try std.testing.expectError(error.ActiveReaders, database.flush());
7241 try std.testing.expectEqual(@as(PublicationToken, 1), try database.preparePublication());
7242 database.publicationCommitted(2);
7243 const checkpoint = try database.checkpoint(.{ .restart_header = recoveredHeader() });
7244
7245 try std.testing.expect(!checkpoint.restarted);
7246 try std.testing.expectEqual(@as(usize, 1), checkpoint.pages);
7247 try std.testing.expectEqual(@as(usize, 2), database.pager.frameCount());
7248 try std.testing.expectEqual(@as(u8, 55), (try testSnapshotPageByte(oldest, 1)).?);
7249 var current_read = try database.beginRead();
7250 defer current_read.deinit();
7251 try std.testing.expectEqual(
7252 @as(u8, 77),
7253 (try testSnapshotPageByte(current_read.snapshot(), 1)).?,
7254 );
7255 }
7256
7257 const restarted = try database.checkpoint(.{ .restart_header = recoveredHeader() });
7258 try std.testing.expect(restarted.restarted);
7259 try std.testing.expectEqual(
7260 @as(u8, 77),
7261 (try testDatabasePageByte(&database, 1)).?,
7262 );
7263 }
7264
7265 test "file database checkpoint after lazy open preserves untouched base pages" {
7266 var tmp = std.testing.tmpDir(.{});
7267 defer tmp.cleanup();
7268
7269 const checkpoint_header: wal.Header = .{
7270 .sequence = 79,
7271 .salt = .{ .first = 0xaa55_aa55, .second = 0x55aa_55aa },
7272 };
7273
7274 {
7275 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7276 .paths = .{ .database = "main.db", .wal = "main.wal" },
7277 .header = testingHeader(),
7278 });
7279 defer database.deinit();
7280
7281 var first: [page.size]u8 = undefined;
7282 var second: [page.size]u8 = undefined;
7283 fillImage(&first, 1, 10);
7284 fillImage(&second, 2, 20);
7285 try database.appendWalSync(1, 0, &first);
7286 try database.appendWalSync(2, 2, &second);
7287 _ = try database.checkpoint(.{ .restart_header = recoveredHeader() });
7288 }
7289
7290 {
7291 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7292 .paths = .{ .database = "main.db", .wal = "main.wal" },
7293 .header = recoveredHeader(),
7294 });
7295 defer database.deinit();
7296
7297 try std.testing.expect(!database.base_loaded);
7298 try std.testing.expectEqual(@as(usize, 0), database.pager.base.items.len);
7299
7300 var third: [page.size]u8 = undefined;
7301 fillImage(&third, 3, 30);
7302 try database.appendWalSync(3, 3, &third);
7303 _ = try database.checkpoint(.{ .restart_header = checkpoint_header });
7304 try std.testing.expect(!database.base_loaded);
7305 try std.testing.expectEqual(@as(usize, 0), database.pager.storage().base_images);
7306 }
7307
7308 var reopened = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7309 .paths = .{ .database = "main.db", .wal = "main.wal" },
7310 .header = checkpoint_header,
7311 });
7312 defer reopened.deinit();
7313
7314 var snapshot_read = try reopened.beginRead();
7315 defer snapshot_read.deinit();
7316 const snapshot = snapshot_read.snapshot();
7317 try std.testing.expectEqual(@as(u8, 10), (try testSnapshotPageByte(snapshot, 1)).?);
7318 try std.testing.expectEqual(@as(u8, 20), (try testSnapshotPageByte(snapshot, 2)).?);
7319 try std.testing.expectEqual(@as(u8, 30), (try testSnapshotPageByte(snapshot, 3)).?);
7320 }
7321
7322 test "file database checkpoint rewrite carries tail to later durable commit" {
7323 var tmp = std.testing.tmpDir(.{});
7324 defer tmp.cleanup();
7325
7326 {
7327 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7328 .paths = .{ .database = "main.db", .wal = "main.wal" },
7329 .header = testingHeader(),
7330 });
7331 defer database.deinit();
7332
7333 var committed: [page.size]u8 = undefined;
7334 var tail: [page.size]u8 = undefined;
7335 var marker: [page.size]u8 = undefined;
7336 fillImage(&committed, 1, 56);
7337 fillImage(&tail, 2, 57);
7338 fillImage(&marker, 3, 58);
7339
7340 try database.appendWalSync(1, 1, &committed);
7341 try database.appendWalSync(2, 0, &tail);
7342 const checkpoint = try database.checkpoint(.{ .restart_header = recoveredHeader() });
7343 var before_read = try database.beginRead();
7344 defer before_read.deinit();
7345 const before_commit = before_read.snapshot();
7346
7347 try std.testing.expect(checkpoint.restarted);
7348 try std.testing.expectEqual(wal.header_size + wal.frame_size, database.wal_written);
7349 try std.testing.expectEqual(@as(usize, 1), database.pager.frameCount());
7350 try std.testing.expectEqual(@as(usize, 0), before_commit.view.end_mark);
7351 try std.testing.expectEqual(@as(u8, 56), (try testSnapshotPageByte(before_commit, 1)).?);
7352 try std.testing.expect(try testSnapshotPageByte(before_commit, 2) == null);
7353
7354 try database.appendWalSync(3, 3, &marker);
7355 var after_read = try database.beginRead();
7356 defer after_read.deinit();
7357 const after_commit = after_read.snapshot();
7358 try std.testing.expectEqual(@as(usize, 2), after_commit.view.end_mark);
7359 try std.testing.expectEqual(@as(u8, 57), (try testSnapshotPageByte(after_commit, 2)).?);
7360 try std.testing.expectEqual(@as(u8, 58), (try testSnapshotPageByte(after_commit, 3)).?);
7361 }
7362
7363 var reopened = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7364 .paths = .{ .database = "main.db", .wal = "main.wal" },
7365 .header = .{ .sequence = 80, .salt = .{ .first = 0xaa55_bb66, .second = 0x55aa_66bb } },
7366 });
7367 defer reopened.deinit();
7368
7369 var snapshot_read = try reopened.beginRead();
7370 defer snapshot_read.deinit();
7371 const snapshot = snapshot_read.snapshot();
7372 try std.testing.expectEqual(@as(u8, 56), (try testSnapshotPageByte(snapshot, 1)).?);
7373 try std.testing.expectEqual(@as(u8, 57), (try testSnapshotPageByte(snapshot, 2)).?);
7374 try std.testing.expectEqual(@as(u8, 58), (try testSnapshotPageByte(snapshot, 3)).?);
7375 try std.testing.expectEqual(@as(usize, 0), snapshot.view.end_mark);
7376 }
7377
7378 test "file database reopen ignores uncommitted appended tail" {
7379 var tmp = std.testing.tmpDir(.{});
7380 defer tmp.cleanup();
7381
7382 {
7383 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7384 .paths = .{ .database = "main.db", .wal = "main.wal" },
7385 .header = testingHeader(),
7386 });
7387 defer database.deinit();
7388
7389 var committed: [page.size]u8 = undefined;
7390 var uncommitted: [page.size]u8 = undefined;
7391 fillImage(&committed, 1, 66);
7392 fillImage(&uncommitted, 1, 99);
7393 try database.appendWal(1, 1, &committed);
7394 try database.appendWal(1, 0, &uncommitted);
7395 try database.syncWal();
7396 }
7397
7398 var reopened = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7399 .paths = .{ .database = "main.db", .wal = "main.wal" },
7400 .header = recoveredHeader(),
7401 });
7402 defer reopened.deinit();
7403
7404 try std.testing.expectEqual(
7405 @as(u8, 66),
7406 (try testDatabasePageByte(&reopened, 1)).?,
7407 );
7408 }
7409
7410 test "file database reopen with matching wal header drops uncommitted appended tail" {
7411 var tmp = std.testing.tmpDir(.{});
7412 defer tmp.cleanup();
7413
7414 {
7415 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7416 .paths = .{ .database = "main.db", .wal = "main.wal" },
7417 .header = testingHeader(),
7418 });
7419 defer database.deinit();
7420
7421 var committed: [page.size]u8 = undefined;
7422 var uncommitted: [page.size]u8 = undefined;
7423 fillImage(&committed, 1, 66);
7424 fillImage(&uncommitted, 2, 99);
7425 try database.appendWal(1, 1, &committed);
7426 try database.appendWal(2, 0, &uncommitted);
7427 try database.syncWal();
7428 }
7429
7430 var reopened = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7431 .paths = .{ .database = "main.db", .wal = "main.wal" },
7432 .header = testingHeader(),
7433 });
7434 defer reopened.deinit();
7435
7436 try std.testing.expectEqual(@as(usize, 1), reopened.pager.frameCount());
7437 try std.testing.expectEqual(@as(usize, 1), (try reopened.pager.currentView()).end_mark);
7438 var tx = try reopened.beginWrite();
7439 defer tx.deinit();
7440 }
7441
7442 test "file transaction transfers staged WAL pages without a page owner" {
7443 comptime {
7444 @stardustClaim(
7445 @import("alloc_phase").capacity.witness(TransactionStaging, "sql_file_transaction_staging_transfer_transitive_risk"),
7446 null,
7447 null,
7448 null,
7449 null,
7450 null,
7451 null,
7452 );
7453 }
7454 comptime {
7455 @stardustClaim(
7456 @import("alloc_phase").capacity.witness(TransactionStaging, "sql_file_transaction_staging_transfer_foreign_risk"),
7457 null,
7458 null,
7459 null,
7460 null,
7461 null,
7462 null,
7463 );
7464 }
7465
7466 var tmp = std.testing.tmpDir(.{});
7467 defer tmp.cleanup();
7468
7469 const max_wal_bytes = wal.header_size + 4 * wal.frame_size;
7470 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7471 .paths = .{ .database = "main.db", .wal = "main.wal" },
7472 .header = testingHeader(),
7473 .max_wal_bytes = max_wal_bytes,
7474 .write_capacity = .{ .wal_frames = 4, .wal_pages = 4 },
7475 });
7476 defer database.deinit();
7477
7478 var before_read = try database.beginRead();
7479 defer before_read.deinit();
7480 const before = before_read.snapshot();
7481 const wal_pointer = database.pager.walBytes().ptr;
7482 const wal_address = @intFromPtr(wal_pointer);
7483 try std.testing.expect(@sizeOf(Transaction) < page.size);
7484 try std.testing.expect(@sizeOf(TransactionStaging) < page.size);
7485
7486 var first: [page.size]u8 = undefined;
7487 var second: [page.size]u8 = undefined;
7488 var third: [page.size]u8 = undefined;
7489 var replacement: [page.size]u8 = undefined;
7490 fillImage(&first, 1, 11);
7491 fillImage(&second, 2, 22);
7492 fillImage(&third, 3, 33);
7493 fillImage(&replacement, 3, 44);
7494
7495 var transaction = try database.beginWrite();
7496 defer transaction.deinit();
7497 try std.testing.expectError(error.WriteTransactionOpen, database.beginWrite());
7498 try std.testing.expect(database.write_transaction_open);
7499 try std.testing.expectEqual(@as(usize, 8), transaction.staging.index.len);
7500
7501 try transaction.putPage(3, &third);
7502 try transaction.putPage(1, &first);
7503 try transaction.putPage(2, &second);
7504 const staged = (try transaction.getPage(3)).?;
7505 const staged_address = @intFromPtr(staged.ptr);
7506 try std.testing.expect(staged_address >= wal_address + wal.header_size);
7507 try std.testing.expect(staged_address + staged.len <= wal_address + max_wal_bytes);
7508 try std.testing.expectEqual(@as(u8, 33), staged[1]);
7509
7510 try transaction.putPage(3, &replacement);
7511 const replaced = (try transaction.getPage(3)).?;
7512 try std.testing.expectEqual(staged.ptr, replaced.ptr);
7513 try std.testing.expectEqual(@as(u8, 44), replaced[1]);
7514 try std.testing.expect(try testSnapshotPageByte(before, 1) == null);
7515
7516 const commit = try transaction.commit(.{ .durability = .buffered });
7517 try std.testing.expectEqual(@as(usize, 3), commit.frames);
7518 try std.testing.expectEqual(@as(usize, 3), commit.pages);
7519 try std.testing.expect(!database.write_transaction_open);
7520 try std.testing.expectEqual(wal_pointer, database.pager.walBytes().ptr);
7521 try std.testing.expect(try testSnapshotPageByte(before, 1) == null);
7522
7523 var reader = try wal.Reader.init(database.pager.walBytes());
7524 const first_frame = (try reader.next()).?;
7525 const second_frame = (try reader.next()).?;
7526 const third_frame = (try reader.next()).?;
7527 try std.testing.expectEqual(@as(u32, 1), first_frame.page_id);
7528 try std.testing.expectEqual(@as(u32, 2), second_frame.page_id);
7529 try std.testing.expectEqual(@as(u32, 3), third_frame.page_id);
7530 try std.testing.expectEqual(@as(u8, 11), first_frame.image[1]);
7531 try std.testing.expectEqual(@as(u8, 22), second_frame.image[1]);
7532 try std.testing.expectEqual(@as(u8, 44), third_frame.image[1]);
7533
7534 var after_read = try database.beginRead();
7535 defer after_read.deinit();
7536 const after = after_read.snapshot();
7537 try std.testing.expectEqual(@as(u8, 11), (try testSnapshotPageByte(after, 1)).?);
7538 try std.testing.expectEqual(@as(u8, 22), (try testSnapshotPageByte(after, 2)).?);
7539 try std.testing.expectEqual(@as(u8, 44), (try testSnapshotPageByte(after, 3)).?);
7540
7541 var next = try database.beginWrite();
7542 next.deinit();
7543 }
7544
7545 test "file database savepoint restores one committed transaction" {
7546 var tmp = std.testing.tmpDir(.{});
7547 defer tmp.cleanup();
7548
7549 {
7550 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7551 .paths = .{ .database = "savepoint.db", .wal = "savepoint.wal" },
7552 .header = testingHeader(),
7553 });
7554 defer database.deinit();
7555
7556 const point = try database.savepoint();
7557 var image: [page.size]u8 = undefined;
7558 fillImage(&image, 1, 77);
7559 var transaction = try database.beginWrite();
7560 defer transaction.deinit();
7561 try transaction.putPage(1, &image);
7562 _ = try transaction.commit(.{ .durability = .buffered });
7563 try std.testing.expectEqual(
7564 @as(u8, 77),
7565 (try testDatabasePageByte(&database, 1)).?,
7566 );
7567 try database.restore(point);
7568 try std.testing.expect(try testDatabasePageByte(&database, 1) == null);
7569 }
7570
7571 var reopened = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7572 .paths = .{ .database = "savepoint.db", .wal = "savepoint.wal" },
7573 .header = testingHeader(),
7574 });
7575 defer reopened.deinit();
7576 try std.testing.expect(try testDatabasePageByte(&reopened, 1) == null);
7577 }
7578
7579 test "file database appends after a restore end the log where they stop" {
7580 var tmp = std.testing.tmpDir(.{});
7581 defer tmp.cleanup();
7582
7583 var image: [page.size]u8 = undefined;
7584 {
7585 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7586 .paths = .{ .database = "append.db", .wal = "append.wal" },
7587 .header = testingHeader(),
7588 });
7589 defer database.deinit();
7590
7591 const point = try database.savepoint();
7592 {
7593 var transaction = try database.beginWrite();
7594 defer transaction.deinit();
7595 for (1..4) |page_id| {
7596 fillImage(&image, @intCast(page_id), 40);
7597 try transaction.putPage(@intCast(page_id), &image);
7598 }
7599 _ = try transaction.commit(.{ .durability = .buffered });
7600 }
7601 const rolled_back_len = try database.wal_file.length(testing_io);
7602 try database.restore(point);
7603 try std.testing.expectEqual(@as(usize, 1), database.wal_io.resizes);
7604 {
7605 var transaction = try database.beginWrite();
7606 defer transaction.deinit();
7607 fillImage(&image, 1, 50);
7608 try transaction.putPage(1, &image);
7609 _ = try transaction.commit(.{ .durability = .buffered });
7610 }
7611 try std.testing.expectEqual(@as(usize, 1), database.wal_io.resizes);
7612 const wal_len = try database.wal_file.length(testing_io);
7613 try std.testing.expectEqual(database.wal_written, wal_len);
7614 try std.testing.expectEqual(database.pager.walBytes().len, wal_len);
7615 try std.testing.expect(wal_len < rolled_back_len);
7616 }
7617
7618 var reopened = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7619 .paths = .{ .database = "append.db", .wal = "append.wal" },
7620 .header = testingHeader(),
7621 });
7622 defer reopened.deinit();
7623 try std.testing.expectEqual(@as(u8, 50), (try testDatabasePageByte(&reopened, 1)).?);
7624 try std.testing.expect(try testDatabasePageByte(&reopened, 2) == null);
7625 try std.testing.expect(try testDatabasePageByte(&reopened, 3) == null);
7626 try std.testing.expectEqual(reopened.wal_written, try reopened.wal_file.length(testing_io));
7627 }
7628
7629 test "file database restores an exact contiguous write sequence" {
7630 var tmp = std.testing.tmpDir(.{});
7631 defer tmp.cleanup();
7632
7633 {
7634 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7635 .paths = .{ .database = "write-sequence.db", .wal = "write-sequence.wal" },
7636 .header = testingHeader(),
7637 });
7638 defer database.deinit();
7639
7640 const before = try database.savepoint();
7641 var first: [page.size]u8 = undefined;
7642 fillImage(&first, 1, 11);
7643 var first_transaction = try database.beginWrite();
7644 defer first_transaction.deinit();
7645 try first_transaction.putPage(1, &first);
7646 _ = try first_transaction.commit(.{ .durability = .buffered });
7647
7648 var second: [page.size]u8 = undefined;
7649 fillImage(&second, 2, 22);
7650 var second_transaction = try database.beginWrite();
7651 defer second_transaction.deinit();
7652 try second_transaction.putPage(2, &second);
7653 _ = try second_transaction.commit(.{ .durability = .buffered });
7654 const after = try database.savepoint();
7655
7656 try database.restoreWrites(before, after);
7657 try std.testing.expect(try testDatabasePageByte(&database, 1) == null);
7658 try std.testing.expect(try testDatabasePageByte(&database, 2) == null);
7659 }
7660
7661 var reopened = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7662 .paths = .{ .database = "write-sequence.db", .wal = "write-sequence.wal" },
7663 .header = testingHeader(),
7664 });
7665 defer reopened.deinit();
7666 try std.testing.expect(try testDatabasePageByte(&reopened, 1) == null);
7667 try std.testing.expect(try testDatabasePageByte(&reopened, 2) == null);
7668 }
7669
7670 test "file database write-sequence restore rejects mixed and stale state" {
7671 var tmp = std.testing.tmpDir(.{});
7672 defer tmp.cleanup();
7673
7674 {
7675 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7676 .paths = .{ .database = "mixed-sequence.db", .wal = "mixed-sequence.wal" },
7677 .header = testingHeader(),
7678 });
7679 defer database.deinit();
7680 const before = try database.savepoint();
7681 var image: [page.size]u8 = undefined;
7682 fillImage(&image, 1, 11);
7683 var transaction = try database.beginWrite();
7684 defer transaction.deinit();
7685 try transaction.putPage(1, &image);
7686 _ = try transaction.commit(.{ .durability = .buffered });
7687 try database.flush();
7688 const after = try database.savepoint();
7689 try std.testing.expectError(
7690 error.TransactionConflict,
7691 database.restoreWrites(before, after),
7692 );
7693 }
7694
7695 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7696 .paths = .{ .database = "stale-sequence.db", .wal = "stale-sequence.wal" },
7697 .header = testingHeader(),
7698 });
7699 defer database.deinit();
7700 const before = try database.savepoint();
7701 var first: [page.size]u8 = undefined;
7702 fillImage(&first, 1, 11);
7703 var first_transaction = try database.beginWrite();
7704 defer first_transaction.deinit();
7705 try first_transaction.putPage(1, &first);
7706 _ = try first_transaction.commit(.{ .durability = .buffered });
7707 const after = try database.savepoint();
7708 var later: [page.size]u8 = undefined;
7709 fillImage(&later, 2, 22);
7710 var later_transaction = try database.beginWrite();
7711 defer later_transaction.deinit();
7712 try later_transaction.putPage(2, &later);
7713 _ = try later_transaction.commit(.{ .durability = .buffered });
7714 try std.testing.expectError(
7715 error.TransactionConflict,
7716 database.restoreWrites(before, after),
7717 );
7718 }
7719
7720 test "file database rejects stale savepoints" {
7721 var tmp = std.testing.tmpDir(.{});
7722 defer tmp.cleanup();
7723
7724 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7725 .paths = .{ .database = "stale.db", .wal = "stale.wal" },
7726 .header = testingHeader(),
7727 });
7728 defer database.deinit();
7729
7730 const multiple = try database.savepoint();
7731 var first: [page.size]u8 = undefined;
7732 fillImage(&first, 1, 11);
7733 var first_transaction = try database.beginWrite();
7734 defer first_transaction.deinit();
7735 try first_transaction.putPage(1, &first);
7736 _ = try first_transaction.commit(.{ .durability = .buffered });
7737
7738 var second: [page.size]u8 = undefined;
7739 fillImage(&second, 2, 22);
7740 var second_transaction = try database.beginWrite();
7741 defer second_transaction.deinit();
7742 try second_transaction.putPage(2, &second);
7743 _ = try second_transaction.commit(.{ .durability = .buffered });
7744 try std.testing.expectError(error.TransactionConflict, database.restore(multiple));
7745
7746 const checkpointed = try database.savepoint();
7747 try database.flush();
7748 try std.testing.expectError(
7749 error.TransactionConflict,
7750 database.restore(checkpointed),
7751 );
7752 try std.testing.expectEqual(
7753 @as(u8, 11),
7754 (try testDatabasePageByte(&database, 1)).?,
7755 );
7756 try std.testing.expectEqual(
7757 @as(u8, 22),
7758 (try testDatabasePageByte(&database, 2)).?,
7759 );
7760 }
7761
7762 test "file transaction commits multiple pages with one commit marker" {
7763 var tmp = std.testing.tmpDir(.{});
7764 defer tmp.cleanup();
7765
7766 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7767 .paths = .{ .database = "main.db", .wal = "main.wal" },
7768 .header = testingHeader(),
7769 });
7770 defer database.deinit();
7771
7772 var first: [page.size]u8 = undefined;
7773 var second: [page.size]u8 = undefined;
7774 fillImage(&first, 1, 11);
7775 fillImage(&second, 2, 22);
7776
7777 var tx = try database.beginWrite();
7778 defer tx.deinit();
7779 try tx.putPage(2, &second);
7780 try tx.putPage(1, &first);
7781 const commit = try tx.commit(.{ .durability = .buffered });
7782
7783 try std.testing.expectEqual(@as(usize, 2), commit.frames);
7784 try std.testing.expectEqual(@as(usize, 2), commit.pages);
7785 try std.testing.expect(!commit.synced);
7786 try std.testing.expectEqual(@as(usize, 2), commit.view.end_mark);
7787 try std.testing.expectEqual(@as(usize, 1), database.wal_io.writes);
7788 try std.testing.expectEqual(@as(usize, 0), database.wal_io.resizes);
7789 try std.testing.expectEqual(@as(usize, 0), database.wal_io.syncs);
7790 try std.testing.expectEqual(database.wal_written, try database.wal_file.length(testing_io));
7791
7792 var reader = try wal.Reader.init(database.pager.walBytes());
7793 const frame_one = (try reader.next()).?;
7794 const frame_two = (try reader.next()).?;
7795 try std.testing.expectEqual(@as(u32, 1), frame_one.page_id);
7796 try std.testing.expectEqual(@as(u32, 0), frame_one.db_page_count);
7797 try std.testing.expectEqual(@as(u32, 2), frame_two.page_id);
7798 try std.testing.expectEqual(@as(u32, 2), frame_two.db_page_count);
7799
7800 var snapshot_read = try database.beginRead();
7801 defer snapshot_read.deinit();
7802 const snapshot = snapshot_read.snapshot();
7803 try std.testing.expectEqual(@as(u8, 11), (try testSnapshotPageByte(snapshot, 1)).?);
7804 try std.testing.expectEqual(@as(u8, 22), (try testSnapshotPageByte(snapshot, 2)).?);
7805 }
7806
7807 test "file transaction coalesces repeated page writes" {
7808 var tmp = std.testing.tmpDir(.{});
7809 defer tmp.cleanup();
7810
7811 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7812 .paths = .{ .database = "main.db", .wal = "main.wal" },
7813 .header = testingHeader(),
7814 });
7815 defer database.deinit();
7816
7817 var first: [page.size]u8 = undefined;
7818 var second: [page.size]u8 = undefined;
7819 fillImage(&first, 1, 31);
7820 fillImage(&second, 1, 32);
7821
7822 var tx = try database.beginWrite();
7823 defer tx.deinit();
7824 try tx.putPage(1, &first);
7825 try tx.putPage(1, &second);
7826 const commit = try tx.commit(.{ .durability = .buffered });
7827
7828 try std.testing.expectEqual(@as(usize, 1), commit.frames);
7829 try std.testing.expectEqual(@as(usize, 1), database.pager.frameCount());
7830 try std.testing.expectEqual(
7831 @as(u8, 32),
7832 (try testDatabasePageByte(&database, 1)).?,
7833 );
7834 }
7835
7836 test "file transaction commits edits made through a staged page" {
7837 var tmp = std.testing.tmpDir(.{});
7838 defer tmp.cleanup();
7839
7840 {
7841 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7842 .paths = .{ .database = "main.db", .wal = "main.wal" },
7843 .header = testingHeader(),
7844 });
7845 defer database.deinit();
7846
7847 var image: [page.size]u8 = undefined;
7848 fillImage(&image, 1, 71);
7849
7850 var tx = try database.beginWrite();
7851 defer tx.deinit();
7852 try std.testing.expectError(error.InvalidPageId, tx.editPage(0));
7853 try std.testing.expect(try tx.editPage(1) == null);
7854 try tx.putPage(1, &image);
7855 const staged = (try tx.editPage(1)).?;
7856 const read = (try tx.getPage(1)).?;
7857 try std.testing.expectEqual(@intFromPtr(read.ptr), @intFromPtr(staged));
7858 staged[1] = 72;
7859 try std.testing.expectEqual(@as(u8, 72), read[1]);
7860 try tx.putPage(1, staged);
7861 _ = try tx.commit(.{});
7862 try std.testing.expectError(error.TransactionClosed, tx.editPage(1));
7863 }
7864
7865 var reopened = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7866 .paths = .{ .database = "main.db", .wal = "main.wal" },
7867 .header = recoveredHeader(),
7868 });
7869 defer reopened.deinit();
7870 try std.testing.expectEqual(@as(u8, 72), (try testDatabasePageByte(&reopened, 1)).?);
7871 }
7872
7873 test "file transaction rejects concurrent pager changes" {
7874 var tmp = std.testing.tmpDir(.{});
7875 defer tmp.cleanup();
7876
7877 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7878 .paths = .{ .database = "main.db", .wal = "main.wal" },
7879 .header = testingHeader(),
7880 });
7881 defer database.deinit();
7882
7883 var transaction_image: [page.size]u8 = undefined;
7884 var outside_image: [page.size]u8 = undefined;
7885 fillImage(&transaction_image, 1, 41);
7886 fillImage(&outside_image, 2, 42);
7887
7888 var tx = try database.beginWrite();
7889 defer tx.deinit();
7890 try tx.putPage(1, &transaction_image);
7891 try database.appendWal(2, 2, &outside_image);
7892
7893 try std.testing.expectError(error.TransactionConflict, tx.commit(.{ .durability = .buffered }));
7894 var snapshot_read = try database.beginRead();
7895 defer snapshot_read.deinit();
7896 const snapshot = snapshot_read.snapshot();
7897 try std.testing.expect(try testSnapshotPageByte(snapshot, 1) == null);
7898 try std.testing.expectEqual(@as(u8, 42), (try testSnapshotPageByte(snapshot, 2)).?);
7899 }
7900
7901 test "file database beginWrite rejects uncommitted wal tail" {
7902 var tmp = std.testing.tmpDir(.{});
7903 defer tmp.cleanup();
7904
7905 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7906 .paths = .{ .database = "main.db", .wal = "main.wal" },
7907 .header = testingHeader(),
7908 });
7909 defer database.deinit();
7910
7911 var image: [page.size]u8 = undefined;
7912 fillImage(&image, 1, 51);
7913 try database.appendWal(1, 0, &image);
7914
7915 try std.testing.expectError(error.UncommittedWalTail, database.beginWrite());
7916 }
7917
7918 test "file transaction synced commit recovers after reopen" {
7919 var tmp = std.testing.tmpDir(.{});
7920 defer tmp.cleanup();
7921
7922 {
7923 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7924 .paths = .{ .database = "main.db", .wal = "main.wal" },
7925 .header = testingHeader(),
7926 });
7927 defer database.deinit();
7928
7929 var first: [page.size]u8 = undefined;
7930 var second: [page.size]u8 = undefined;
7931 fillImage(&first, 1, 61);
7932 fillImage(&second, 2, 62);
7933
7934 var tx = try database.beginWrite();
7935 defer tx.deinit();
7936 try tx.putPage(1, &first);
7937 try tx.putPage(2, &second);
7938 const commit = try tx.commit(.{});
7939 try std.testing.expect(commit.synced);
7940 }
7941
7942 var reopened = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7943 .paths = .{ .database = "main.db", .wal = "main.wal" },
7944 .header = recoveredHeader(),
7945 });
7946 defer reopened.deinit();
7947
7948 var snapshot_read = try reopened.beginRead();
7949 defer snapshot_read.deinit();
7950 const snapshot = snapshot_read.snapshot();
7951 try std.testing.expectEqual(@as(u8, 61), (try testSnapshotPageByte(snapshot, 1)).?);
7952 try std.testing.expectEqual(@as(u8, 62), (try testSnapshotPageByte(snapshot, 2)).?);
7953 try std.testing.expectEqual(wal.header_size, reopened.wal_written);
7954 }
7955
7956 test "file database rejects a wal limit smaller than the header" {
7957 var tmp = std.testing.tmpDir(.{});
7958 defer tmp.cleanup();
7959
7960 try std.testing.expectError(error.InvalidWalLimit, Database.openForTesting(std.testing.allocator, tmp.dir, .{
7961 .paths = .{ .database = "main.db", .wal = "main.wal" },
7962 .header = testingHeader(),
7963 .max_wal_bytes = wal.header_size - 1,
7964 }));
7965 }
7966
7967 test "file database separates wal recovery limit from byte capacity" {
7968 var tmp = std.testing.tmpDir(.{});
7969 defer tmp.cleanup();
7970
7971 const one_frame_bytes = wal.header_size + wal.frame_size;
7972 const two_frame_bytes = wal.header_size + 2 * wal.frame_size;
7973 const four_frame_bytes = wal.header_size + 4 * wal.frame_size;
7974 {
7975 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7976 .paths = .{ .database = "main.db", .wal = "main.wal" },
7977 .header = testingHeader(),
7978 .max_wal_bytes = four_frame_bytes,
7979 .wal_capacity_bytes = two_frame_bytes,
7980 });
7981 defer database.deinit();
7982 try std.testing.expectEqual(two_frame_bytes, database.walCapacityBytes());
7983
7984 var image: [page.size]u8 = undefined;
7985 fillImage(&image, 1, 61);
7986 var transaction = try database.beginWrite();
7987 defer transaction.deinit();
7988 try transaction.putPage(1, &image);
7989 _ = try transaction.commit(.{});
7990 }
7991
7992 {
7993 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
7994 .paths = .{ .database = "main.db", .wal = "main.wal" },
7995 .header = testingHeader(),
7996 .max_wal_bytes = four_frame_bytes,
7997 .wal_capacity_bytes = two_frame_bytes,
7998 });
7999 defer database.deinit();
8000 var snapshot_read = try database.beginRead();
8001 defer snapshot_read.deinit();
8002 const snapshot = snapshot_read.snapshot();
8003 try std.testing.expectEqual(@as(u8, 61), (try testSnapshotPageByte(snapshot, 1)).?);
8004
8005 var image: [page.size]u8 = undefined;
8006 fillImage(&image, 2, 62);
8007 var transaction = try database.beginWrite();
8008 defer transaction.deinit();
8009 try transaction.putPage(2, &image);
8010 _ = try transaction.commit(.{});
8011
8012 var blocked = try database.beginWrite();
8013 defer blocked.deinit();
8014 try std.testing.expectError(error.WalLimitExceeded, blocked.putPage(3, &image));
8015 }
8016
8017 try std.testing.expectError(error.WalFull, Database.openForTesting(std.testing.allocator, tmp.dir, .{
8018 .paths = .{ .database = "main.db", .wal = "main.wal" },
8019 .header = testingHeader(),
8020 .max_wal_bytes = four_frame_bytes,
8021 .wal_capacity_bytes = one_frame_bytes,
8022 }));
8023 try std.testing.expectError(error.InvalidWalLimit, Database.openForTesting(std.testing.allocator, tmp.dir, .{
8024 .paths = .{ .database = "main.db", .wal = "main.wal" },
8025 .header = testingHeader(),
8026 .max_wal_bytes = two_frame_bytes,
8027 .wal_capacity_bytes = four_frame_bytes,
8028 }));
8029
8030 var empty_tmp = std.testing.tmpDir(.{});
8031 defer empty_tmp.cleanup();
8032 var empty = try Database.openForTesting(std.testing.allocator, empty_tmp.dir, .{
8033 .paths = .{ .database = "main.db", .wal = "main.wal" },
8034 .header = testingHeader(),
8035 .max_wal_bytes = four_frame_bytes,
8036 .wal_capacity_bytes = wal.header_size,
8037 });
8038 defer empty.deinit();
8039 try std.testing.expectEqual(wal.header_size, empty.walCapacityBytes());
8040 var image: [page.size]u8 = undefined;
8041 fillImage(&image, 1, 63);
8042 var blocked = try empty.beginWrite();
8043 defer blocked.deinit();
8044 try std.testing.expectError(error.TransactionTooLarge, blocked.putPage(1, &image));
8045 }
8046
8047 test "file database wal bound survives checkpoint rotation and reopen" {
8048 var tmp = std.testing.tmpDir(.{});
8049 defer tmp.cleanup();
8050
8051 const max_wal_bytes = wal.header_size + 2 * wal.frame_size;
8052 {
8053 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
8054 .paths = .{ .database = "main.db", .wal = "main.wal" },
8055 .header = testingHeader(),
8056 .max_wal_bytes = max_wal_bytes,
8057 });
8058 defer database.deinit();
8059
8060 var first: [page.size]u8 = undefined;
8061 var second: [page.size]u8 = undefined;
8062 var third: [page.size]u8 = undefined;
8063 fillImage(&first, 1, 71);
8064 fillImage(&second, 2, 72);
8065 fillImage(&third, 3, 73);
8066
8067 var initial = try database.beginWrite();
8068 defer initial.deinit();
8069 try initial.putPage(1, &first);
8070 try initial.putPage(2, &second);
8071 _ = try initial.commit(.{});
8072 try std.testing.expectEqual(max_wal_bytes, database.wal_written);
8073
8074 var blocked = try database.beginWrite();
8075 errdefer blocked.deinit();
8076 try std.testing.expectError(error.WalLimitExceeded, blocked.putPage(3, &third));
8077 blocked.deinit();
8078 try std.testing.expectEqual(max_wal_bytes, database.wal_written);
8079 try std.testing.expectEqual(max_wal_bytes, database.pager.walBytes().len);
8080
8081 const checkpoint_value = try database.checkpoint(.{ .restart_header = recoveredHeader() });
8082 try std.testing.expect(checkpoint_value.restarted);
8083 try std.testing.expectEqual(wal.header_size, database.wal_written);
8084
8085 var resumed = try database.beginWrite();
8086 defer resumed.deinit();
8087 try resumed.putPage(3, &third);
8088 _ = try resumed.commit(.{});
8089 try std.testing.expect(database.wal_written <= database.max_wal_bytes);
8090 }
8091
8092 var reopened = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
8093 .paths = .{ .database = "main.db", .wal = "main.wal" },
8094 .header = recoveredHeader(),
8095 .max_wal_bytes = max_wal_bytes,
8096 });
8097 defer reopened.deinit();
8098 var snapshot_read = try reopened.beginRead();
8099 defer snapshot_read.deinit();
8100 const snapshot = snapshot_read.snapshot();
8101 try std.testing.expectEqual(@as(u8, 71), (try testSnapshotPageByte(snapshot, 1)).?);
8102 try std.testing.expectEqual(@as(u8, 72), (try testSnapshotPageByte(snapshot, 2)).?);
8103 try std.testing.expectEqual(@as(u8, 73), (try testSnapshotPageByte(snapshot, 3)).?);
8104 try std.testing.expect(reopened.wal_written <= reopened.max_wal_bytes);
8105 }
8106
8107 test "file database repeated near limit commits always reopen" {
8108 var tmp = std.testing.tmpDir(.{});
8109 defer tmp.cleanup();
8110
8111 const max_wal_bytes = wal.header_size + 3 * wal.frame_size;
8112 const cycles = 8;
8113 for (0..cycles) |cycle| {
8114 {
8115 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
8116 .paths = .{ .database = "main.db", .wal = "main.wal" },
8117 .header = testingHeader(),
8118 .max_wal_bytes = max_wal_bytes,
8119 });
8120 defer database.deinit();
8121
8122 const first_page: u32 = @intCast(cycle * 2 + 1);
8123 const second_page = first_page + 1;
8124 var first: [page.size]u8 = undefined;
8125 var second: [page.size]u8 = undefined;
8126 fillImage(&first, first_page, @intCast(first_page));
8127 fillImage(&second, second_page, @intCast(second_page));
8128
8129 var transaction = try database.beginWrite();
8130 var rotate = false;
8131 transaction.putPage(first_page, &first) catch |err| switch (err) {
8132 error.WalLimitExceeded => rotate = true,
8133 else => return err,
8134 };
8135 if (!rotate) transaction.putPage(second_page, &second) catch |err| switch (err) {
8136 error.WalLimitExceeded => rotate = true,
8137 else => return err,
8138 };
8139 if (!rotate) if (transaction.commit(.{})) |_| {} else |err| switch (err) {
8140 error.WalLimitExceeded => rotate = true,
8141 else => return err,
8142 };
8143 transaction.deinit();
8144
8145 if (rotate) {
8146 const checkpoint_value = try database.checkpoint(.{ .restart_header = testingHeader() });
8147 try std.testing.expect(checkpoint_value.restarted);
8148 var retry = try database.beginWrite();
8149 defer retry.deinit();
8150 try retry.putPage(first_page, &first);
8151 try retry.putPage(second_page, &second);
8152 _ = try retry.commit(.{});
8153 }
8154 try std.testing.expect(database.wal_written <= max_wal_bytes);
8155 try std.testing.expectEqual(database.wal_written, try database.wal_file.length(testing_io));
8156 }
8157
8158 var reopened = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
8159 .paths = .{ .database = "main.db", .wal = "main.wal" },
8160 .header = testingHeader(),
8161 .max_wal_bytes = max_wal_bytes,
8162 });
8163 defer reopened.deinit();
8164 var snapshot_read = try reopened.beginRead();
8165 defer snapshot_read.deinit();
8166 const snapshot = snapshot_read.snapshot();
8167 var page_id: u32 = 1;
8168 while (page_id <= (cycle + 1) * 2) : (page_id += 1) {
8169 try std.testing.expectEqual(@as(u8, @intCast(page_id)), (try testSnapshotPageByte(snapshot, page_id)).?);
8170 }
8171 try std.testing.expect(reopened.wal_written <= max_wal_bytes);
8172 }
8173 }
8174
8175 test "file transaction larger than the wal bound leaves no tail" {
8176 var tmp = std.testing.tmpDir(.{});
8177 defer tmp.cleanup();
8178
8179 const max_wal_bytes = wal.header_size + wal.frame_size;
8180 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
8181 .paths = .{ .database = "main.db", .wal = "main.wal" },
8182 .header = testingHeader(),
8183 .max_wal_bytes = max_wal_bytes,
8184 });
8185 defer database.deinit();
8186
8187 var first: [page.size]u8 = undefined;
8188 var second: [page.size]u8 = undefined;
8189 fillImage(&first, 1, 81);
8190 fillImage(&second, 2, 82);
8191
8192 var oversized = try database.beginWrite();
8193 errdefer oversized.deinit();
8194 try oversized.putPage(1, &first);
8195 try std.testing.expectError(error.TransactionTooLarge, oversized.putPage(2, &second));
8196 oversized.deinit();
8197 try std.testing.expectEqual(wal.header_size, database.wal_written);
8198 try std.testing.expectEqual(@as(usize, 0), database.pager.frameCount());
8199
8200 var fitting = try database.beginWrite();
8201 defer fitting.deinit();
8202 try fitting.putPage(1, &first);
8203 _ = try fitting.commit(.{});
8204 try std.testing.expectEqual(max_wal_bytes, database.wal_written);
8205 }
8206
8207 test "file recovery discards a torn transaction batch" {
8208 var tmp = std.testing.tmpDir(.{});
8209 defer tmp.cleanup();
8210
8211 {
8212 var database = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
8213 .paths = .{ .database = "main.db", .wal = "main.wal" },
8214 .header = testingHeader(),
8215 });
8216 defer database.deinit();
8217
8218 var first: [page.size]u8 = undefined;
8219 var second: [page.size]u8 = undefined;
8220 var third: [page.size]u8 = undefined;
8221 fillImage(&first, 1, 91);
8222 fillImage(&second, 2, 92);
8223 fillImage(&third, 3, 93);
8224
8225 var stable = try database.beginWrite();
8226 defer stable.deinit();
8227 try stable.putPage(1, &first);
8228 _ = try stable.commit(.{});
8229
8230 var torn = try database.beginWrite();
8231 defer torn.deinit();
8232 try torn.putPage(2, &second);
8233 try torn.putPage(3, &third);
8234 _ = try torn.commit(.{});
8235 }
8236
8237 {
8238 var wal_file = try tmp.dir.openFile(testing_io, "main.wal", .{ .mode = .read_write });
8239 defer wal_file.close(testing_io);
8240 const torn_length = wal.header_size + 2 * wal.frame_size + wal.frame_header_size + 97;
8241 try wal_file.setLength(testing_io, torn_length);
8242 try wal_file.sync(testing_io);
8243 }
8244
8245 var reopened = try Database.openForTesting(std.testing.allocator, tmp.dir, .{
8246 .paths = .{ .database = "main.db", .wal = "main.wal" },
8247 .header = testingHeader(),
8248 });
8249 defer reopened.deinit();
8250 var snapshot_read = try reopened.beginRead();
8251 defer snapshot_read.deinit();
8252 const snapshot = snapshot_read.snapshot();
8253 try std.testing.expectEqual(@as(u8, 91), (try testSnapshotPageByte(snapshot, 1)).?);
8254 try std.testing.expect(try testSnapshotPageByte(snapshot, 2) == null);
8255 try std.testing.expect(try testSnapshotPageByte(snapshot, 3) == null);
8256 try std.testing.expectEqual(wal.header_size + wal.frame_size, reopened.wal_written);
8257 try std.testing.expectEqual(reopened.wal_written, try reopened.wal_file.length(testing_io));
8258
8259 var appended: [page.size]u8 = undefined;
8260 fillImage(&appended, 2, 94);
8261 var append = try reopened.beginWrite();
8262 defer append.deinit();
8263 try append.putPage(2, &appended);
8264 _ = try append.commit(.{ .durability = .buffered });
8265 try std.testing.expectEqual(wal.header_size + 2 * wal.frame_size, reopened.wal_written);
8266 try std.testing.expectEqual(reopened.wal_written, try reopened.wal_file.length(testing_io));
8267 }