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 }