lib/sql/src/lifecycle.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const builtin = @import("builtin");
   3 const branch = @import("branch.zig");
   4 const catalog_mod = @import("catalog.zig");
   5 const connection_mod = @import("connection.zig");
   6 const file_mod = @import("file.zig");
   7 const history_mod = @import("history/root.zig");
   8 const pager = @import("pager.zig");
   9 const session_mod = @import("session/root.zig");
  10 const version = @import("version.zig");
  11 const wal = @import("wal.zig");
  12 
  13 const Allocator = std.mem.Allocator;
  14 const default_capacity: pager.Capacity = .{ .wal_frames = 8192 };
  15 
  16 /// Owns one database and the storage that database runs on, created by a tool
  17 /// at startup to open its database through it. The workspace holds the file
  18 /// workspace and, once opened, the database itself, and hands callers a pointer
  19 /// to that database. Because callers hold that pointer, the workspace has to
  20 /// stay at one address from the open until the opened database is released, so
  21 /// callers place it on the heap. The workspace takes its storage either from
  22 /// the caller with `init` or from an allocator with `allocate`, and gives it
  23 /// back with the matching `deinit` or `deallocate`. The workspace opens one
  24 /// database at a time, reports `WorkspaceBusy` for a second open while the
  25 /// first is live, and refuses teardown by assertion while a database is still
  26 /// open.
  27 pub const Workspace = struct {
  28     pub const Limits = file_mod.Database.Workspace.Limits;
  29     pub const Capacity = file_mod.Database.Workspace.Capacity;
  30     pub const Storage = file_mod.Database.Workspace.Storage;
  31     pub const InitError = file_mod.Database.Workspace.InitError;
  32     pub const AllocateError = file_mod.Database.Workspace.AllocateError;
  33     pub const AcquireError = file_mod.Database.Workspace.AcquireError;
  34 
  35     file_workspace: file_mod.Database.Workspace,
  36     database: file_mod.Database = undefined,
  37     database_live: bool = false,
  38 
  39     pub fn init(
  40         storage: Storage,
  41         limits: Limits,
  42     ) InitError!Workspace {
  43         return .{
  44             .file_workspace = try file_mod.Database.Workspace.init(storage, limits),
  45         };
  46     }
  47 
  48     pub fn allocate(allocator: Allocator, limits: Limits) AllocateError!Workspace {
  49         return .{
  50             .file_workspace = try file_mod.Database.Workspace.allocate(allocator, limits),
  51         };
  52     }
  53 
  54     pub fn activate(self: *Workspace) void {
  55         std.debug.assert(!self.database_live);
  56         self.file_workspace.activate();
  57     }
  58 
  59     pub fn deinit(self: *Workspace) Storage {
  60         std.debug.assert(!self.database_live);
  61         const storage = self.file_workspace.deinit();
  62         self.* = undefined;
  63         return storage;
  64     }
  65 
  66     pub fn deallocate(self: *Workspace, allocator: Allocator) void {
  67         std.debug.assert(!self.database_live);
  68         self.file_workspace.deallocate(allocator);
  69         self.* = undefined;
  70     }
  71 
  72     pub fn retainedBytes(self: *const Workspace) usize {
  73         return self.file_workspace.retainedBytes();
  74     }
  75 
  76     fn acquireWritable(
  77         self: *Workspace,
  78         allocator: Allocator,
  79         dir: std.Io.Dir,
  80         options: file_mod.OpenOptions,
  81     ) file_mod.Error!*file_mod.Database {
  82         if (self.database_live) return error.WorkspaceBusy;
  83         const writable = try file_mod.Database.open(
  84             allocator,
  85             &self.file_workspace,
  86             dir,
  87             options,
  88         );
  89         self.database = writable;
  90         self.database_live = true;
  91         return &self.database;
  92     }
  93 
  94     fn acquireReadOnly(
  95         self: *Workspace,
  96         allocator: Allocator,
  97         dir: std.Io.Dir,
  98         options: file_mod.ReadOpenOptions,
  99     ) file_mod.Error!*file_mod.Database {
 100         if (self.database_live) return error.WorkspaceBusy;
 101         self.database = try file_mod.Database.openReadOnly(
 102             allocator,
 103             &self.file_workspace,
 104             dir,
 105             options,
 106         );
 107         self.database_live = true;
 108         return &self.database;
 109     }
 110 
 111     fn releaseDatabase(self: *Workspace, database: *file_mod.Database) void {
 112         std.debug.assert(self.database_live);
 113         std.debug.assert(database == &self.database);
 114         database.deinit();
 115         self.database_live = false;
 116     }
 117 
 118     fn ownerOf(writable: *file_mod.Database) *Workspace {
 119         const owner: *Workspace = @alignCast(@fieldParentPtr(
 120             "file_workspace",
 121             writable.workspace,
 122         ));
 123         std.debug.assert(owner.database_live);
 124         std.debug.assert(writable == &owner.database);
 125         return owner;
 126     }
 127 };
 128 
 129 pub const Error = file_mod.Error || history_mod.Error || history_mod.refs.ReadError || connection_mod.Error || std.Io.Dir.DeleteFileError;
 130 
 131 pub const Options = struct {
 132     io: std.Io = std.Options.debug_io,
 133     paths: file_mod.Paths = .{},
 134     history_path: []const u8,
 135     branch: []const u8 = "main",
 136     header: wal.Header,
 137     max_wal_bytes: usize = file_mod.default_max_wal_bytes,
 138     wal_capacity_bytes: ?usize = null,
 139     capacity: pager.Capacity = default_capacity,
 140     control: wal.Control = .{},
 141 };
 142 
 143 pub const ReadOptions = struct {
 144     io: std.Io = std.Options.debug_io,
 145     paths: file_mod.Paths = .{},
 146     history_path: []const u8,
 147     branch: []const u8 = "main",
 148     header: wal.Header,
 149     max_wal_bytes: usize = file_mod.default_max_wal_bytes,
 150     read_cache_capacity: usize = file_mod.default_read_cache_capacity,
 151     advance_refs: bool = false,
 152 };
 153 
 154 pub const Recovery = enum {
 155     clean,
 156     created,
 157     truncated_history,
 158     rebuilt_invalid_history,
 159     rebuilt_missing_branch,
 160 };
 161 
 162 pub const LazyHistory = struct {
 163     allocator: Allocator,
 164     io: std.Io,
 165     dir: std.Io.Dir,
 166     path: []u8,
 167     state: State,
 168     retired: ?history_mod.refs.Snapshot = null,
 169     read_only: bool = false,
 170 
 171     const State = union(enum) {
 172         deferred: history_mod.refs.Snapshot,
 173         open: history_mod.History,
 174     };
 175 
 176     pub fn full(self: *LazyHistory) Error!*history_mod.History {
 177         switch (self.state) {
 178             .open => {},
 179             .deferred => |snapshot| {
 180                 std.debug.assert(self.retired == null);
 181                 const opened = try history_mod.History.open(self.allocator, self.dir, .{
 182                     .io = self.io,
 183                     .path = self.path,
 184                     .recovery = .reject,
 185                     .create = !self.read_only,
 186                     .read_only = self.read_only,
 187                 });
 188                 self.retired = snapshot;
 189                 self.state = .{ .open = opened };
 190             },
 191         }
 192         return &self.state.open;
 193     }
 194 
 195     pub fn replayed(self: *const LazyHistory) bool {
 196         return self.state == .open;
 197     }
 198 
 199     pub fn checkoutBranch(self: *const LazyHistory, name: []const u8) Error!branch.Checkout {
 200         return switch (self.state) {
 201             .open => |*history| try history.checkoutBranch(name),
 202             .deferred => |*snapshot| checkout: {
 203                 const entry = snapshot.find(name) orelse return error.RefNotFound;
 204                 break :checkout branch.Checkout.init(
 205                     .{ .name = entry.name, .target = entry.head },
 206                     entry.root,
 207                 );
 208             },
 209         };
 210     }
 211 
 212     pub fn deinit(self: *LazyHistory) void {
 213         switch (self.state) {
 214             .open => |*history| history.deinit(),
 215             .deferred => |*snapshot| snapshot.deinit(),
 216         }
 217         if (self.retired) |*snapshot| snapshot.deinit();
 218         self.dir.close(self.io);
 219         self.allocator.free(self.path);
 220         self.* = undefined;
 221     }
 222 };
 223 
 224 pub const Opened = struct {
 225     allocator: Allocator,
 226     workspace: *Workspace,
 227     workspace_owned: bool = false,
 228     file: *file_mod.Database,
 229     history: LazyHistory,
 230     connection: connection_mod.Connection,
 231     recovery: Recovery,
 232 
 233     pub fn deinit(self: *Opened) void {
 234         const allocator = self.allocator;
 235         const workspace = self.workspace;
 236         const workspace_owned = self.workspace_owned;
 237         close(self.file, &self.history, &self.connection);
 238         if (workspace_owned) {
 239             workspace.deallocate(allocator);
 240             allocator.destroy(workspace);
 241         }
 242         self.* = undefined;
 243     }
 244 };
 245 
 246 pub const ReadRepairReason = union(enum) {
 247     file: file_mod.ReadRepairReason,
 248     missing_refs,
 249     invalid_refs,
 250     stale_refs,
 251     missing_branch,
 252     root_mismatch,
 253 };
 254 
 255 pub const ReadOpened = struct {
 256     allocator: Allocator,
 257     workspace: *Workspace,
 258     workspace_owned: bool = false,
 259     file: *file_mod.ReadOnlyDatabase,
 260     catalog: catalog_mod.Reader,
 261     head: version.Hash,
 262     root: version.Hash,
 263 
 264     pub fn deinit(self: *ReadOpened) void {
 265         const allocator = self.allocator;
 266         const workspace = self.workspace;
 267         const workspace_owned = self.workspace_owned;
 268         self.file.deinit();
 269         self.allocator.destroy(self.file);
 270         if (workspace_owned) {
 271             workspace.deallocate(allocator);
 272             allocator.destroy(workspace);
 273         }
 274         self.* = undefined;
 275     }
 276 };
 277 
 278 pub const ReadOpenResult = union(enum) {
 279     ready: ReadOpened,
 280     repair_required: ReadRepairReason,
 281 };
 282 
 283 pub const ConnectionReadOpenResult = union(enum) {
 284     ready: Opened,
 285     repair_required: ReadRepairReason,
 286 };
 287 
 288 pub const ReadRefsRefreshResult = union(enum) {
 289     current,
 290     refreshed,
 291     moved,
 292     repair_required: ReadRepairReason,
 293 };
 294 
 295 pub const ReadRefsPrepareResult = union(enum) {
 296     current,
 297     candidate: history_mod.refs.Snapshot,
 298     moved,
 299     repair_required: ReadRepairReason,
 300 };
 301 
 302 pub const SparseReadOptions = struct {
 303     read: ReadOptions,
 304     control: wal.Control = .{},
 305 };
 306 
 307 const SparseBranch = struct {
 308     covered_length: u64,
 309     head: version.Hash,
 310     root: version.Hash,
 311     conflicts: version.Hash,
 312 };
 313 
 314 pub const SparseReadAssessment = struct {
 315     catalog: catalog_mod.Reader,
 316     head: version.Hash,
 317     root: version.Hash,
 318     branch: SparseBranch,
 319     live_root: version.Hash,
 320 };
 321 
 322 pub const SparseReadBeginResult = union(enum) {
 323     ready: SparseReadAssessment,
 324     moved,
 325     repair_required: ReadRepairReason,
 326 };
 327 
 328 pub const SparseReadFinishResult = union(enum) {
 329     stable,
 330     moved,
 331     repair_required: ReadRepairReason,
 332 };
 333 
 334 const SparseRefreshResult = union(enum) {
 335     ready: std.meta.Tag(file_mod.SparseRefreshResult),
 336     repair_required: ReadRepairReason,
 337 };
 338 
 339 pub const SparseReadSession = struct {
 340     workspace: wal.Scanner.Workspace,
 341     file: file_mod.SparseReadOnlyDatabase = undefined,
 342     file_live: bool = false,
 343 
 344     pub fn init(self: *SparseReadSession, workspace: wal.Scanner.Workspace) void {
 345         self.* = .{ .workspace = workspace };
 346     }
 347 
 348     pub fn deinit(self: *SparseReadSession) void {
 349         self.resetFile();
 350         self.* = undefined;
 351     }
 352 
 353     pub fn begin(
 354         self: *SparseReadSession,
 355         allocator: Allocator,
 356         dir: std.Io.Dir,
 357         options: SparseReadOptions,
 358     ) Error!SparseReadBeginResult {
 359         try options.control.check();
 360         const branch_state = switch (try loadSparseBranch(allocator, dir, options.read, options.control)) {
 361             .ready => |ready| ready,
 362             .repair_required => |reason| return .{ .repair_required = reason },
 363         };
 364         switch (try self.refreshFile(dir, options)) {
 365             .ready => {},
 366             .repair_required => |reason| return .{ .repair_required = reason },
 367         }
 368         try options.control.check();
 369         const catalog = catalog_mod.Reader.open(self.file.snapshot(), .{}) catch |err| {
 370             if (err == error.RecoveryRequired) {
 371                 self.resetFile();
 372                 return .moved;
 373             }
 374             return err;
 375         };
 376         var live_root = version.readDatabaseRootMaintained(
 377             allocator,
 378             &catalog,
 379             branch_state.conflicts,
 380         ) catch |err| {
 381             if (err == error.RecoveryRequired) {
 382                 self.resetFile();
 383                 return .moved;
 384             }
 385             return err;
 386         };
 387         defer live_root.deinit();
 388         try options.control.check();
 389         return .{ .ready = .{
 390             .catalog = catalog,
 391             .head = branch_state.head,
 392             .root = branch_state.root,
 393             .branch = branch_state,
 394             .live_root = live_root.hash,
 395         } };
 396     }
 397 
 398     pub fn finish(
 399         self: *SparseReadSession,
 400         allocator: Allocator,
 401         dir: std.Io.Dir,
 402         assessment: SparseReadAssessment,
 403         options: SparseReadOptions,
 404     ) Error!SparseReadFinishResult {
 405         try options.control.check();
 406         const file_moved = switch (try self.refreshFile(dir, options)) {
 407             .ready => |state| state == .refreshed,
 408             .repair_required => |reason| return .{ .repair_required = reason },
 409         };
 410         const branch_state = switch (try loadSparseBranch(allocator, dir, options.read, options.control)) {
 411             .ready => |ready| ready,
 412             .repair_required => |reason| return .{ .repair_required = reason },
 413         };
 414         const catalog = catalog_mod.Reader.open(self.file.snapshot(), .{}) catch |err| {
 415             if (err == error.RecoveryRequired) {
 416                 self.resetFile();
 417                 return .moved;
 418             }
 419             return err;
 420         };
 421         var live_root = version.readDatabaseRootMaintained(
 422             allocator,
 423             &catalog,
 424             branch_state.conflicts,
 425         ) catch |err| {
 426             if (err == error.RecoveryRequired) {
 427                 self.resetFile();
 428                 return .moved;
 429             }
 430             return err;
 431         };
 432         defer live_root.deinit();
 433         try options.control.check();
 434         if (file_moved or
 435             !std.meta.eql(branch_state, assessment.branch) or
 436             !version.same(live_root.hash, assessment.live_root))
 437         {
 438             return .moved;
 439         }
 440         if (!version.same(live_root.hash, branch_state.root)) {
 441             return .{ .repair_required = .root_mismatch };
 442         }
 443         return .stable;
 444     }
 445 
 446     fn refreshFile(
 447         self: *SparseReadSession,
 448         dir: std.Io.Dir,
 449         options: SparseReadOptions,
 450     ) Error!SparseRefreshResult {
 451         if (!self.file_live) {
 452             const opened = file_mod.SparseReadOnlyDatabase.openExisting(dir, .{
 453                 .io = options.read.io,
 454                 .paths = options.read.paths,
 455                 .workspace = self.workspace,
 456                 .control = options.control,
 457             }) catch |err| {
 458                 self.resetFile();
 459                 return err;
 460             };
 461             switch (opened) {
 462                 .ready => |ready| {
 463                     self.file = ready;
 464                     self.file_live = true;
 465                     return .{ .ready = .refreshed };
 466                 },
 467                 .repair_required => |reason| return .{
 468                     .repair_required = .{ .file = reason },
 469                 },
 470             }
 471         }
 472         const refreshed = self.file.refresh(dir, .{
 473             .paths = options.read.paths,
 474             .control = options.control,
 475         }) catch |err| {
 476             self.resetFile();
 477             return err;
 478         };
 479         return switch (refreshed) {
 480             .unchanged => .{ .ready = .unchanged },
 481             .refreshed => .{ .ready = .refreshed },
 482             .repair_required => |reason| repair: {
 483                 self.resetFile();
 484                 break :repair .{ .repair_required = .{ .file = reason } };
 485             },
 486         };
 487     }
 488 
 489     fn resetFile(self: *SparseReadSession) void {
 490         if (self.file_live) self.file.deinit();
 491         self.file_live = false;
 492     }
 493 };
 494 
 495 pub fn open(
 496     allocator: Allocator,
 497     workspace: *Workspace,
 498     dir: std.Io.Dir,
 499     options: Options,
 500 ) Error!Opened {
 501     var recovery: Recovery = if (fileExists(options.io, dir, options.history_path)) .clean else .created;
 502     const file_ptr = try workspace.acquireWritable(allocator, dir, .{
 503         .io = options.io,
 504         .paths = options.paths,
 505         .header = options.header,
 506         .max_wal_bytes = options.max_wal_bytes,
 507         .wal_capacity_bytes = options.wal_capacity_bytes,
 508         .write_capacity = options.capacity,
 509         .control = options.control,
 510     });
 511     errdefer workspace.releaseDatabase(file_ptr);
 512 
 513     var owned_dir = try dir.openDir(options.io, ".", .{});
 514     errdefer owned_dir.close(options.io);
 515     const owned_path = try allocator.dupe(u8, options.history_path);
 516     errdefer allocator.free(owned_path);
 517 
 518     if (recovery == .clean) {
 519         if (try deferredCheckout(allocator, dir, options)) |deferred| {
 520             var snapshot = deferred.snapshot;
 521             errdefer snapshot.deinit();
 522             var connection = try liveConnection(
 523                 allocator,
 524                 file_ptr,
 525                 deferred.checkout,
 526                 deferred.conflicts,
 527             );
 528             if (version.same((try connection.workingRoot()), deferred.checkout.working.working)) {
 529                 return .{
 530                     .allocator = allocator,
 531                     .workspace = workspace,
 532                     .file = file_ptr,
 533                     .history = .{
 534                         .allocator = allocator,
 535                         .io = options.io,
 536                         .dir = owned_dir,
 537                         .path = owned_path,
 538                         .state = .{ .deferred = snapshot },
 539                     },
 540                     .connection = connection,
 541                     .recovery = .clean,
 542                 };
 543             }
 544             connection.deinit();
 545             snapshot.deinit();
 546         }
 547     }
 548 
 549     var history = history_mod.History.open(allocator, dir, .{
 550         .io = options.io,
 551         .path = options.history_path,
 552         .recovery = .truncate,
 553         .control = options.control,
 554     }) catch |err| switch (err) {
 555         error.InvalidHistory => blk: {
 556             recovery = .rebuilt_invalid_history;
 557             try dir.deleteFile(options.io, options.history_path);
 558             break :blk try history_mod.History.open(allocator, dir, .{
 559                 .io = options.io,
 560                 .path = options.history_path,
 561                 .recovery = .truncate,
 562                 .control = options.control,
 563             });
 564         },
 565         else => return err,
 566     };
 567     errdefer history.deinit();
 568     if (history.recovery == .truncated) recovery = .truncated_history;
 569 
 570     var connection = switch (recovery) {
 571         .created, .rebuilt_invalid_history => try connection_mod.Connection.create(
 572             allocator,
 573             file_ptr,
 574             &history,
 575             .{ .branch = options.branch },
 576         ),
 577         .clean, .truncated_history, .rebuilt_missing_branch => replayedConnection(
 578             allocator,
 579             file_ptr,
 580             &history,
 581             options.branch,
 582         ) catch |err| switch (err) {
 583             error.RefNotFound => blk: {
 584                 recovery = .rebuilt_missing_branch;
 585                 break :blk try connection_mod.Connection.create(allocator, file_ptr, &history, .{ .branch = options.branch });
 586             },
 587             else => return err,
 588         },
 589     };
 590     errdefer connection.deinit();
 591 
 592     return .{
 593         .allocator = allocator,
 594         .workspace = workspace,
 595         .file = file_ptr,
 596         .history = .{
 597             .allocator = allocator,
 598             .io = options.io,
 599             .dir = owned_dir,
 600             .path = owned_path,
 601             .state = .{ .open = history },
 602         },
 603         .connection = connection,
 604         .recovery = recovery,
 605     };
 606 }
 607 
 608 pub fn openForTesting(allocator: Allocator, dir: std.Io.Dir, options: Options) Error!Opened {
 609     if (!builtin.is_test) @compileError("openForTesting is available only in tests");
 610     const workspace = try allocator.create(Workspace);
 611     errdefer allocator.destroy(workspace);
 612     workspace.* = try Workspace.allocate(allocator, .{
 613         .header = options.header,
 614         .max_wal_bytes = options.wal_capacity_bytes orelse options.max_wal_bytes,
 615         .path_storage = file_mod.PathStorage.Limits.forDirect(options.paths),
 616     });
 617     errdefer workspace.deallocate(allocator);
 618     var opened = try open(allocator, workspace, dir, options);
 619     opened.workspace_owned = true;
 620     return opened;
 621 }
 622 
 623 pub fn openReadOnly(
 624     allocator: Allocator,
 625     workspace: *Workspace,
 626     dir: std.Io.Dir,
 627     options: ReadOptions,
 628 ) Error!ReadOpenResult {
 629     var refs_snapshot = switch (try loadReadRefs(allocator, dir, options, .{})) {
 630         .ready => |ready| ready,
 631         .repair_required => |reason| return .{ .repair_required = reason },
 632     };
 633     defer refs_snapshot.deinit();
 634     return try openReadOnlyFromSnapshot(
 635         allocator,
 636         workspace,
 637         dir,
 638         options,
 639         &refs_snapshot,
 640         .{},
 641     );
 642 }
 643 
 644 pub fn openConnectionReadOnly(
 645     allocator: Allocator,
 646     workspace: *Workspace,
 647     dir: std.Io.Dir,
 648     options: ReadOptions,
 649 ) Error!ConnectionReadOpenResult {
 650     var snapshot = switch (try loadReadRefs(allocator, dir, options, .{})) {
 651         .ready => |ready| ready,
 652         .repair_required => |reason| return .{ .repair_required = reason },
 653     };
 654     var snapshot_live = true;
 655     defer if (snapshot_live) snapshot.deinit();
 656     const entry = snapshot.find(options.branch) orelse
 657         return .{ .repair_required = .missing_branch };
 658     const file_ptr = try workspace.acquireReadOnly(allocator, dir, .{
 659         .io = options.io,
 660         .paths = options.paths,
 661         .header = options.header,
 662         .max_wal_bytes = options.max_wal_bytes,
 663         .read_cache_capacity = options.read_cache_capacity,
 664     });
 665     var file_live = true;
 666     defer if (file_live) workspace.releaseDatabase(file_ptr);
 667     var connection = try liveConnection(
 668         allocator,
 669         file_ptr,
 670         branch.Checkout.init(.{ .name = entry.name, .target = entry.head }, entry.root),
 671         entry.conflicts,
 672     );
 673     var connection_live = true;
 674     defer if (connection_live) connection.deinit();
 675     if (!version.same(try connection.workingRoot(), entry.root)) {
 676         return .{ .repair_required = .root_mismatch };
 677     }
 678     var owned_dir = try dir.openDir(options.io, ".", .{});
 679     errdefer owned_dir.close(options.io);
 680     const owned_path = try allocator.dupe(u8, options.history_path);
 681     snapshot_live = false;
 682     file_live = false;
 683     connection_live = false;
 684     return .{ .ready = .{
 685         .allocator = allocator,
 686         .workspace = workspace,
 687         .file = file_ptr,
 688         .history = .{
 689             .allocator = allocator,
 690             .io = options.io,
 691             .dir = owned_dir,
 692             .path = owned_path,
 693             .state = .{ .deferred = snapshot },
 694             .read_only = true,
 695         },
 696         .connection = connection,
 697         .recovery = .clean,
 698     } };
 699 }
 700 
 701 fn openReadOnlyFromSnapshot(
 702     allocator: Allocator,
 703     workspace: *Workspace,
 704     dir: std.Io.Dir,
 705     options: ReadOptions,
 706     refs_snapshot: *const history_mod.refs.Snapshot,
 707     control: wal.Control,
 708 ) Error!ReadOpenResult {
 709     try control.check();
 710     const entry = refs_snapshot.find(options.branch) orelse
 711         return .{ .repair_required = .missing_branch };
 712 
 713     var database_value = switch (try file_mod.ReadOnlyDatabase.openExisting(
 714         allocator,
 715         &workspace.file_workspace,
 716         dir,
 717         .{
 718             .io = options.io,
 719             .paths = options.paths,
 720             .header = options.header,
 721             .max_wal_bytes = options.max_wal_bytes,
 722             .read_cache_capacity = options.read_cache_capacity,
 723             .control = control,
 724         },
 725     )) {
 726         .ready => |ready| ready,
 727         .repair_required => |reason| return .{ .repair_required = .{ .file = reason } },
 728     };
 729     const database = allocator.create(file_mod.ReadOnlyDatabase) catch |err| {
 730         database_value.deinit();
 731         return err;
 732     };
 733     database.* = database_value;
 734     var database_live = true;
 735     defer if (database_live) {
 736         database.deinit();
 737         allocator.destroy(database);
 738     };
 739 
 740     try control.check();
 741     const catalog = try catalog_mod.Reader.open(database.snapshot(), .{});
 742     var live_root = try version.readDatabaseRootMaintained(allocator, &catalog, entry.conflicts);
 743     defer live_root.deinit();
 744     try control.check();
 745     if (!version.same(live_root.hash, entry.root)) {
 746         return .{ .repair_required = .root_mismatch };
 747     }
 748 
 749     database_live = false;
 750     return .{ .ready = .{
 751         .allocator = allocator,
 752         .workspace = workspace,
 753         .file = database,
 754         .catalog = catalog,
 755         .head = entry.head,
 756         .root = entry.root,
 757     } };
 758 }
 759 
 760 pub fn prepareReadRefs(
 761     allocator: Allocator,
 762     workspace: *Workspace,
 763     dir: std.Io.Dir,
 764     options: ReadOptions,
 765     control: wal.Control,
 766 ) Error!ReadRefsPrepareResult {
 767     try control.check();
 768     var path_buffer: [history_mod.refs.path_bytes_max]u8 = undefined;
 769     if (history_mod.refs.pathFor(&path_buffer, options.history_path) == null) {
 770         return .{ .repair_required = .invalid_refs };
 771     }
 772     var snapshot = (history_mod.refs.loadExisting(
 773         allocator,
 774         options.io,
 775         dir,
 776         options.history_path,
 777     ) catch |err| switch (err) {
 778         error.FileNotFound => return .{ .repair_required = .missing_refs },
 779         else => return err,
 780     }) orelse return .{ .repair_required = .invalid_refs };
 781     defer snapshot.deinit();
 782     const history_stat = dir.statFile(
 783         options.io,
 784         options.history_path,
 785         .{},
 786     ) catch |err| switch (err) {
 787         error.FileNotFound => return .{ .repair_required = .stale_refs },
 788         else => return err,
 789     };
 790     if (snapshot.covered_length == history_stat.size) {
 791         const reason = try validateReadRefs(
 792             allocator,
 793             workspace,
 794             dir,
 795             options,
 796             &snapshot,
 797             control,
 798         );
 799         if (reason) |value| return .{ .repair_required = value };
 800         try control.check();
 801         const final_stat = dir.statFile(options.io, options.history_path, .{}) catch
 802             return .moved;
 803         return if (final_stat.size == history_stat.size) .current else .moved;
 804     }
 805     if (snapshot.covered_length > history_stat.size) {
 806         return .{ .repair_required = .stale_refs };
 807     }
 808     return prepareStaleReadRefs(
 809         allocator,
 810         workspace,
 811         dir,
 812         options,
 813         &snapshot,
 814         history_stat.size,
 815         control,
 816     );
 817 }
 818 
 819 fn prepareStaleReadRefs(
 820     allocator: Allocator,
 821     workspace: *Workspace,
 822     dir: std.Io.Dir,
 823     options: ReadOptions,
 824     snapshot: *const history_mod.refs.Snapshot,
 825     history_length: u64,
 826     control: wal.Control,
 827 ) Error!ReadRefsPrepareResult {
 828     var candidate = history_mod.refs.advance(
 829         allocator,
 830         options.io,
 831         dir,
 832         options.history_path,
 833         snapshot,
 834         history_length,
 835         control,
 836     ) catch |err| switch (err) {
 837         error.InvalidHistory,
 838         error.TruncatedHistory,
 839         error.RecoveryRequired,
 840         error.StreamTooLong,
 841         => return .{ .repair_required = .stale_refs },
 842         else => return err,
 843     };
 844     var candidate_live = true;
 845     defer if (candidate_live) candidate.deinit();
 846     if (try validateReadRefs(
 847         allocator,
 848         workspace,
 849         dir,
 850         options,
 851         &candidate,
 852         control,
 853     )) |reason| return .{ .repair_required = reason };
 854     const final_stat = dir.statFile(options.io, options.history_path, .{}) catch
 855         return .moved;
 856     if (final_stat.size != history_length) return .moved;
 857     try control.check();
 858     candidate_live = false;
 859     return .{ .candidate = candidate };
 860 }
 861 
 862 fn refreshReadRefs(
 863     allocator: Allocator,
 864     workspace: *Workspace,
 865     dir: std.Io.Dir,
 866     options: ReadOptions,
 867     control: wal.Control,
 868 ) Error!ReadRefsRefreshResult {
 869     if (!builtin.is_test) @compileError("read refs publication is tracker-owned");
 870     return switch (try prepareReadRefs(
 871         allocator,
 872         workspace,
 873         dir,
 874         options,
 875         control,
 876     )) {
 877         .current => .current,
 878         .candidate => |candidate_value| publish: {
 879             var candidate = candidate_value;
 880             defer candidate.deinit();
 881             try history_mod.refs.store(
 882                 allocator,
 883                 options.io,
 884                 dir,
 885                 options.history_path,
 886                 candidate.covered_length,
 887                 candidate.entries,
 888             );
 889             break :publish .refreshed;
 890         },
 891         .moved => .moved,
 892         .repair_required => |reason| .{ .repair_required = reason },
 893     };
 894 }
 895 
 896 fn validateReadRefs(
 897     allocator: Allocator,
 898     workspace: *Workspace,
 899     dir: std.Io.Dir,
 900     options: ReadOptions,
 901     snapshot: *const history_mod.refs.Snapshot,
 902     control: wal.Control,
 903 ) Error!?ReadRepairReason {
 904     return switch (try openReadOnlyFromSnapshot(
 905         allocator,
 906         workspace,
 907         dir,
 908         options,
 909         snapshot,
 910         control,
 911     )) {
 912         .ready => |ready_value| ready: {
 913             var ready = ready_value;
 914             ready.deinit();
 915             break :ready null;
 916         },
 917         .repair_required => |reason| reason,
 918     };
 919 }
 920 
 921 pub fn openReadOnlyForTesting(
 922     allocator: Allocator,
 923     dir: std.Io.Dir,
 924     options: ReadOptions,
 925 ) Error!ReadOpenResult {
 926     if (!builtin.is_test) @compileError("openReadOnlyForTesting is available only in tests");
 927     const workspace = try allocator.create(Workspace);
 928     errdefer allocator.destroy(workspace);
 929     workspace.* = try Workspace.allocate(allocator, .{
 930         .header = options.header,
 931         .max_wal_bytes = options.max_wal_bytes,
 932         .path_storage = .{
 933             .database_bytes = 0,
 934             .wal_bytes = 0,
 935         },
 936         .read_cache_pages = options.read_cache_capacity,
 937     });
 938     errdefer workspace.deallocate(allocator);
 939     const result = try openReadOnly(allocator, workspace, dir, options);
 940     return switch (result) {
 941         .ready => |ready| opened: {
 942             var value = ready;
 943             value.workspace_owned = true;
 944             break :opened .{ .ready = value };
 945         },
 946         .repair_required => |reason| repair: {
 947             workspace.deallocate(allocator);
 948             allocator.destroy(workspace);
 949             break :repair .{ .repair_required = reason };
 950         },
 951     };
 952 }
 953 
 954 pub fn close(
 955     file_ptr: *file_mod.Database,
 956     history: *LazyHistory,
 957     connection: *connection_mod.Connection,
 958 ) void {
 959     connection.deinit();
 960     history.deinit();
 961     Workspace.ownerOf(file_ptr).releaseDatabase(file_ptr);
 962 }
 963 
 964 pub fn commit(
 965     connection: *connection_mod.Connection,
 966     history: *LazyHistory,
 967     file_ptr: *file_mod.Database,
 968 ) Error!version.Hash {
 969     var healed = false;
 970     const replay = history.full() catch |err| switch (err) {
 971         error.InvalidHistory, error.TruncatedHistory => blk: {
 972             healed = true;
 973             break :blk try rebuiltHistory(connection, history);
 974         },
 975         else => return err,
 976     };
 977     try connection.stage();
 978     const head = connection.commit(replay) catch |err| switch (err) {
 979         error.NoStagedRoot => head: {
 980             if (!healed) return err;
 981             break :head (try connection.checkout()).head;
 982         },
 983         else => return err,
 984     };
 985     try file_ptr.syncWal();
 986     return head;
 987 }
 988 
 989 fn rebuiltHistory(
 990     connection: *connection_mod.Connection,
 991     history: *LazyHistory,
 992 ) Error!*history_mod.History {
 993     std.debug.assert(!history.replayed());
 994     history.dir.deleteFile(history.io, history.path) catch |err| switch (err) {
 995         error.FileNotFound => {},
 996         else => return err,
 997     };
 998     const branch_name = (try connection.checkout()).name;
 999     const fresh = try history_mod.History.open(history.allocator, history.dir, .{
1000         .io = history.io,
1001         .path = history.path,
1002         .recovery = .reject,
1003     });
1004     const snapshot = history.state.deferred;
1005     history.state = .{ .open = fresh };
1006     std.debug.assert(history.retired == null);
1007     history.retired = snapshot;
1008     const opened = &history.state.open;
1009     try connection.adoptRebuiltHistory(opened, branch_name);
1010     return opened;
1011 }
1012 
1013 pub fn mergeCommit(
1014     connection: *connection_mod.Connection,
1015     history: *LazyHistory,
1016     file_ptr: *file_mod.Database,
1017     theirs: version.Hash,
1018 ) Error!version.Hash {
1019     const replay = try history.full();
1020     const head = try connection.mergeCommit(replay, theirs);
1021     try file_ptr.syncWal();
1022     return head;
1023 }
1024 
1025 pub fn alignToBranchHead(
1026     allocator: Allocator,
1027     connection: *connection_mod.Connection,
1028     history: *LazyHistory,
1029     file_ptr: *file_mod.Database,
1030     branch_name: []const u8,
1031 ) Error!void {
1032     try connection.checkoutBranch(allocator, try history.full(), branch_name);
1033     try file_ptr.syncWal();
1034 }
1035 
1036 pub fn fastForwardBranch(
1037     allocator: Allocator,
1038     connection: *connection_mod.Connection,
1039     history: *LazyHistory,
1040     target: version.Hash,
1041 ) Error!void {
1042     try connection.fastForwardBranch(allocator, try history.full(), target);
1043 }
1044 
1045 pub fn walSize(file_ptr: *const file_mod.Database) usize {
1046     return file_ptr.pager.walBytes().len;
1047 }
1048 
1049 pub fn checkpoint(file_ptr: *file_mod.Database, header: wal.Header) Error!void {
1050     _ = try file_ptr.checkpoint(.{ .restart_header = header });
1051 }
1052 
1053 pub fn checkpointIfWalLarge(file_ptr: *file_mod.Database, header: wal.Header, threshold: usize) Error!void {
1054     if (walSize(file_ptr) >= threshold) try checkpoint(file_ptr, header);
1055 }
1056 
1057 const DeferredCheckout = struct {
1058     snapshot: history_mod.refs.Snapshot,
1059     checkout: branch.Checkout,
1060     conflicts: version.Hash,
1061 };
1062 
1063 const ReadRefsResult = union(enum) {
1064     ready: history_mod.refs.Snapshot,
1065     repair_required: ReadRepairReason,
1066 };
1067 
1068 const SparseBranchResult = union(enum) {
1069     ready: SparseBranch,
1070     repair_required: ReadRepairReason,
1071 };
1072 
1073 fn loadSparseBranch(
1074     allocator: Allocator,
1075     dir: std.Io.Dir,
1076     options: ReadOptions,
1077     control: wal.Control,
1078 ) Error!SparseBranchResult {
1079     var snapshot = switch (try loadReadRefs(allocator, dir, options, control)) {
1080         .ready => |ready| ready,
1081         .repair_required => |reason| return .{ .repair_required = reason },
1082     };
1083     defer snapshot.deinit();
1084     const entry = snapshot.find(options.branch) orelse
1085         return .{ .repair_required = .missing_branch };
1086     return .{ .ready = .{
1087         .covered_length = snapshot.covered_length,
1088         .head = entry.head,
1089         .root = entry.root,
1090         .conflicts = entry.conflicts,
1091     } };
1092 }
1093 
1094 fn loadReadRefs(
1095     allocator: Allocator,
1096     dir: std.Io.Dir,
1097     options: ReadOptions,
1098     control: wal.Control,
1099 ) Error!ReadRefsResult {
1100     try control.check();
1101     var path_buffer: [history_mod.refs.path_bytes_max]u8 = undefined;
1102     if (history_mod.refs.pathFor(&path_buffer, options.history_path) == null) {
1103         return .{ .repair_required = .invalid_refs };
1104     }
1105     var snapshot = (history_mod.refs.loadExisting(
1106         allocator,
1107         options.io,
1108         dir,
1109         options.history_path,
1110     ) catch |err| switch (err) {
1111         error.FileNotFound => return .{ .repair_required = .missing_refs },
1112         else => return err,
1113     }) orelse return .{ .repair_required = .invalid_refs };
1114     var snapshot_live = true;
1115     defer if (snapshot_live) snapshot.deinit();
1116     const history_stat = dir.statFile(
1117         options.io,
1118         options.history_path,
1119         .{},
1120     ) catch |err| switch (err) {
1121         error.FileNotFound => return .{ .repair_required = .stale_refs },
1122         else => return err,
1123     };
1124     if (snapshot.covered_length != history_stat.size) {
1125         if (!options.advance_refs or snapshot.covered_length > history_stat.size) {
1126             return .{ .repair_required = .stale_refs };
1127         }
1128         const advanced = history_mod.refs.advance(
1129             allocator,
1130             options.io,
1131             dir,
1132             options.history_path,
1133             &snapshot,
1134             history_stat.size,
1135             control,
1136         ) catch |err| switch (err) {
1137             error.InvalidHistory,
1138             error.TruncatedHistory,
1139             error.RecoveryRequired,
1140             error.StreamTooLong,
1141             => return .{ .repair_required = .stale_refs },
1142             else => return err,
1143         };
1144         snapshot.deinit();
1145         snapshot = advanced;
1146         try control.check();
1147         const final_stat = try dir.statFile(options.io, options.history_path, .{});
1148         if (final_stat.size != history_stat.size) {
1149             return .{ .repair_required = .stale_refs };
1150         }
1151     }
1152     snapshot_live = false;
1153     return .{ .ready = snapshot };
1154 }
1155 
1156 fn deferredCheckout(allocator: Allocator, dir: std.Io.Dir, options: Options) Error!?DeferredCheckout {
1157     var snapshot = (try history_mod.refs.load(allocator, options.io, dir, options.history_path)) orelse return null;
1158     var snapshot_live = true;
1159     defer if (snapshot_live) snapshot.deinit();
1160     const stat = dir.statFile(options.io, options.history_path, .{}) catch return null;
1161     if (snapshot.covered_length != stat.size) return null;
1162     const entry = snapshot.find(options.branch) orelse return null;
1163     snapshot_live = false;
1164     return .{
1165         .snapshot = snapshot,
1166         .checkout = branch.Checkout.init(.{ .name = entry.name, .target = entry.head }, entry.root),
1167         .conflicts = entry.conflicts,
1168     };
1169 }
1170 
1171 fn liveConnection(
1172     allocator: Allocator,
1173     file_ptr: *file_mod.Database,
1174     checkout: branch.Checkout,
1175     conflicts: version.Hash,
1176 ) Error!connection_mod.Connection {
1177     const catalog = try catalog_mod.Catalog.open(file_ptr, .{});
1178     var root = try version.databaseRootMaintained(allocator, &catalog, conflicts);
1179     errdefer root.deinit();
1180     const live_checkout = checkout.withWorking(root.hash);
1181     const session = session_mod.DatabaseSession.initWithRoot(allocator, live_checkout, &root);
1182     return connection_mod.Connection.init(catalog, session);
1183 }
1184 
1185 fn replayedConnection(
1186     allocator: Allocator,
1187     file_ptr: *file_mod.Database,
1188     history: *history_mod.History,
1189     branch_name: []const u8,
1190 ) Error!connection_mod.Connection {
1191     return try connection_mod.Connection.open(allocator, file_ptr, history, .{
1192         .branch = branch_name,
1193     });
1194 }
1195 
1196 fn fileExists(io: std.Io, dir: std.Io.Dir, path: []const u8) bool {
1197     _ = dir.statFile(io, path, .{}) catch return false;
1198     return true;
1199 }
1200 
1201 const testing_io = std.Options.debug_io;
1202 
1203 fn testingHeader() wal.Header {
1204     return .{
1205         .sequence = 91,
1206         .salt = .{ .first = 0x0101_2323, .second = 0x4545_6767 },
1207     };
1208 }
1209 
1210 fn testingOptions() Options {
1211     return .{
1212         .io = testing_io,
1213         .paths = .{ .database = "live.db", .wal = "live.wal" },
1214         .history_path = "live.history",
1215         .header = testingHeader(),
1216     };
1217 }
1218 
1219 fn testingReadOptions() ReadOptions {
1220     return .{
1221         .io = testing_io,
1222         .paths = .{ .database = "live.db", .wal = "live.wal" },
1223         .history_path = "live.history",
1224         .header = testingHeader(),
1225     };
1226 }
1227 
1228 fn commitTestingRow(opened: *Opened, allocator: Allocator, statement: []const u8) !version.Hash {
1229     var result = try opened.connection.execute(allocator, statement, .{ .durability = .buffered });
1230     result.deinit(allocator);
1231     return try commit(&opened.connection, &opened.history, opened.file);
1232 }
1233 
1234 const TestingReadState = struct {
1235     head: version.Hash,
1236     root: version.Hash,
1237 };
1238 
1239 const TestingControl = struct {
1240     calls: usize = 0,
1241     interrupt_at: ?usize = null,
1242 
1243     fn control(self: *TestingControl) wal.Control {
1244         return .{ .context = self, .interrupted_fn = interrupted };
1245     }
1246 
1247     fn interrupted(context: ?*anyopaque) bool {
1248         const self: *TestingControl = @ptrCast(@alignCast(context.?));
1249         const call = self.calls;
1250         self.calls += 1;
1251         return if (self.interrupt_at) |interrupt_at| call == interrupt_at else false;
1252     }
1253 };
1254 
1255 const TestingHistoryMove = struct {
1256     history: *history_mod.History,
1257     root: version.DatabaseRoot,
1258     move_at: usize,
1259     calls: usize = 0,
1260     moved: bool = false,
1261     failed: bool = false,
1262 
1263     fn control(self: *TestingHistoryMove) wal.Control {
1264         return .{ .context = self, .interrupted_fn = interrupted };
1265     }
1266 
1267     fn interrupted(context: ?*anyopaque) bool {
1268         const self: *TestingHistoryMove = @ptrCast(@alignCast(context.?));
1269         const call = self.calls;
1270         self.calls += 1;
1271         if (call == self.move_at) {
1272             self.history.putDatabaseRoot(self.root) catch {
1273                 self.failed = true;
1274                 return false;
1275             };
1276             self.moved = true;
1277         }
1278         return false;
1279     }
1280 };
1281 
1282 fn overwriteTestingRefs(dir: std.Io.Dir, bytes: []const u8) !void {
1283     var file = try dir.createFile(
1284         testing_io,
1285         "live.history.refs",
1286         .{ .read = true, .truncate = true },
1287     );
1288     defer file.close(testing_io);
1289     try file.writePositionalAll(testing_io, bytes, 0);
1290 }
1291 
1292 fn createTestingReadState(allocator: Allocator, dir: std.Io.Dir) !TestingReadState {
1293     var opened = try openForTesting(allocator, dir, testingOptions());
1294     defer opened.deinit();
1295     const head = try commitTestingRow(&opened, allocator, "CREATE TABLE items (name)");
1296     return .{ .head = head, .root = try opened.connection.workingRoot() };
1297 }
1298 
1299 fn readRepairReason(dir: std.Io.Dir, options: ReadOptions) !ReadRepairReason {
1300     return switch (try openReadOnlyForTesting(std.testing.allocator, dir, options)) {
1301         .repair_required => |reason| reason,
1302         .ready => |ready_value| {
1303             var ready = ready_value;
1304             ready.deinit();
1305             return error.ExpectedRepairRequired;
1306         },
1307     };
1308 }
1309 
1310 fn expectReadRepairTag(
1311     dir: std.Io.Dir,
1312     options: ReadOptions,
1313     expected: std.meta.Tag(ReadRepairReason),
1314 ) !void {
1315     try std.testing.expectEqual(expected, std.meta.activeTag(try readRepairReason(dir, options)));
1316 }
1317 
1318 const TestingConflictHistory = struct {
1319     commit: version.Hash,
1320     database: version.Hash,
1321     conflicts: version.Hash,
1322     artifact: version.Hash,
1323 };
1324 
1325 fn createTestingConflictHistory(allocator: Allocator, dir: std.Io.Dir) !TestingConflictHistory {
1326     var opened = try openForTesting(allocator, dir, testingOptions());
1327     defer opened.deinit();
1328     _ = try commitTestingRow(&opened, allocator, "CREATE TABLE items (name)");
1329     _ = try commitTestingRow(
1330         &opened,
1331         allocator,
1332         "INSERT INTO items (rowid, name) VALUES (1, 'base')",
1333     );
1334     _ = try opened.connection.createBranch(try opened.history.full(), "side");
1335     _ = try commitTestingRow(
1336         &opened,
1337         allocator,
1338         "UPDATE items SET name = 'ours' WHERE rowid = 1",
1339     );
1340     try alignToBranchHead(allocator, &opened.connection, &opened.history, opened.file, "side");
1341     const side = try commitTestingRow(
1342         &opened,
1343         allocator,
1344         "UPDATE items SET name = 'theirs' WHERE rowid = 1",
1345     );
1346     try alignToBranchHead(allocator, &opened.connection, &opened.history, opened.file, "main");
1347     var merged = try opened.connection.mergeBranch(
1348         allocator,
1349         try opened.history.full(),
1350         "side",
1351         .{},
1352     );
1353     defer merged.deinit();
1354     return .{
1355         .commit = try mergeCommit(&opened.connection, &opened.history, opened.file, side),
1356         .database = try opened.connection.workingRoot(),
1357         .conflicts = merged.conflict_root.hash,
1358         .artifact = merged.artifacts[0].hash,
1359     };
1360 }
1361 
1362 test "lifecycle workspace reuses one pinned writable slot" {
1363     const allocator = std.testing.allocator;
1364     var tmp = std.testing.tmpDir(.{});
1365     defer tmp.cleanup();
1366     const options = testingOptions();
1367     var workspace = try Workspace.allocate(allocator, .{
1368         .header = options.header,
1369         .max_wal_bytes = options.wal_capacity_bytes orelse options.max_wal_bytes,
1370         .path_storage = file_mod.PathStorage.Limits.forDirect(options.paths),
1371     });
1372     defer workspace.deallocate(allocator);
1373 
1374     var first = try open(allocator, &workspace, tmp.dir, options);
1375     const slot = first.file;
1376     try std.testing.expectEqual(&workspace.database, slot);
1377     try std.testing.expectError(
1378         error.WorkspaceBusy,
1379         open(allocator, &workspace, tmp.dir, options),
1380     );
1381     first.deinit();
1382 
1383     var second = try open(allocator, &workspace, tmp.dir, options);
1384     defer second.deinit();
1385     try std.testing.expectEqual(slot, second.file);
1386 }
1387 
1388 test "lifecycle read-only open owns a stable maintained snapshot" {
1389     const allocator = std.testing.allocator;
1390     var tmp = std.testing.tmpDir(.{});
1391     defer tmp.cleanup();
1392     const expected = try createTestingReadState(allocator, tmp.dir);
1393 
1394     var opened = switch (try openReadOnlyForTesting(allocator, tmp.dir, testingReadOptions())) {
1395         .ready => |ready| ready,
1396         .repair_required => return error.UnexpectedRepairRequired,
1397     };
1398     defer opened.deinit();
1399     try std.testing.expect(version.same(expected.head, opened.head));
1400     try std.testing.expect(version.same(expected.root, opened.root));
1401     var names = try opened.catalog.relationNames(allocator);
1402     defer names.deinit();
1403     try std.testing.expectEqual(@as(usize, 1), names.names.len);
1404     try std.testing.expectEqualSlices(u8, "items", names.names[0]);
1405 }
1406 
1407 test "lifecycle read connection derives stale refs and rejects durable writes" {
1408     const allocator = std.testing.allocator;
1409     var tmp = std.testing.tmpDir(.{});
1410     defer tmp.cleanup();
1411     _ = try createTestingReadState(allocator, tmp.dir);
1412     const stale = try tmp.dir.readFileAlloc(
1413         testing_io,
1414         "live.history.refs",
1415         allocator,
1416         .limited(1 << 20),
1417     );
1418     defer allocator.free(stale);
1419     var expected: version.Hash = undefined;
1420     {
1421         var writer = try openForTesting(allocator, tmp.dir, testingOptions());
1422         defer writer.deinit();
1423         expected = try commitTestingRow(
1424             &writer,
1425             allocator,
1426             "INSERT INTO items (rowid, name) VALUES (2, 'later')",
1427         );
1428     }
1429     try overwriteTestingRefs(tmp.dir, stale);
1430     const before = try readArtifactMetadata(tmp.dir);
1431     var options = testingReadOptions();
1432     options.advance_refs = true;
1433     var workspace = try Workspace.allocate(allocator, .{
1434         .header = options.header,
1435         .max_wal_bytes = options.max_wal_bytes,
1436         .path_storage = file_mod.PathStorage.Limits.forDirect(options.paths),
1437     });
1438     defer workspace.deallocate(allocator);
1439     {
1440         var opened = switch (try openConnectionReadOnly(allocator, &workspace, tmp.dir, options)) {
1441             .ready => |ready| ready,
1442             .repair_required => return error.UnexpectedRepairRequired,
1443         };
1444         defer opened.deinit();
1445         try std.testing.expect(version.same(expected, opened.connection.session.checkout.head));
1446         const history = try opened.history.full();
1447         try std.testing.expect(version.same(expected, (try history.checkoutBranch("main")).head));
1448         try std.testing.expectError(error.ReadOnly, history.createBranch("forbidden", expected));
1449         try std.testing.expectError(error.ReadOnlyDatabase, opened.connection.execute(
1450             allocator,
1451             "INSERT INTO items (rowid, name) VALUES (3, 'forbidden')",
1452             .{},
1453         ));
1454     }
1455     try std.testing.expectEqualDeep(before, try readArtifactMetadata(tmp.dir));
1456     const after = try tmp.dir.readFileAlloc(
1457         testing_io,
1458         "live.history.refs",
1459         allocator,
1460         .limited(1 << 20),
1461     );
1462     defer allocator.free(after);
1463     try std.testing.expectEqualSlices(u8, stale, after);
1464 }
1465 
1466 const ReadArtifactMetadata = struct {
1467     size: u64,
1468     mtime: std.Io.Timestamp,
1469     ctime: std.Io.Timestamp,
1470 };
1471 
1472 fn readArtifactMetadata(dir: std.Io.Dir) ![4]ReadArtifactMetadata {
1473     const paths = [_][]const u8{
1474         "live.db", "live.wal", "live.history", "live.history.refs",
1475     };
1476     var values: [paths.len]ReadArtifactMetadata = undefined;
1477     for (paths, &values) |path, *value| {
1478         const stat = try dir.statFile(testing_io, path, .{});
1479         value.* = .{ .size = stat.size, .mtime = stat.mtime, .ctime = stat.ctime };
1480     }
1481     return values;
1482 }
1483 
1484 test "lifecycle read-only forwards read cache capacity" {
1485     const allocator = std.testing.allocator;
1486     var tmp = std.testing.tmpDir(.{});
1487     defer tmp.cleanup();
1488     _ = try createTestingReadState(allocator, tmp.dir);
1489 
1490     var options = testingReadOptions();
1491     options.read_cache_capacity = 3;
1492     var opened = switch (try openReadOnlyForTesting(allocator, tmp.dir, options)) {
1493         .ready => |ready| ready,
1494         .repair_required => return error.UnexpectedRepairRequired,
1495     };
1496     defer opened.deinit();
1497     try std.testing.expectEqual(@as(usize, 3), opened.file.readCacheCapacity());
1498 }
1499 
1500 test "lifecycle late read refs cancellation preserves the stale sidecar" {
1501     const allocator = std.testing.allocator;
1502     var tmp = std.testing.tmpDir(.{});
1503     defer tmp.cleanup();
1504     const options = testingOptions();
1505     {
1506         var opened = try openForTesting(allocator, tmp.dir, options);
1507         defer opened.deinit();
1508         _ = try commitTestingRow(&opened, allocator, "CREATE TABLE items (name)");
1509     }
1510     const stale = try tmp.dir.readFileAlloc(
1511         testing_io,
1512         "live.history.refs",
1513         allocator,
1514         .unlimited,
1515     );
1516     defer allocator.free(stale);
1517     {
1518         var opened = try openForTesting(allocator, tmp.dir, options);
1519         defer opened.deinit();
1520         _ = try commitTestingRow(
1521             &opened,
1522             allocator,
1523             "INSERT INTO items (rowid, name) VALUES (1, 'later')",
1524         );
1525     }
1526     var workspace = try Workspace.allocate(allocator, .{
1527         .header = options.header,
1528         .max_wal_bytes = options.max_wal_bytes,
1529         .path_storage = .{ .database_bytes = 0, .wal_bytes = 0 },
1530         .read_cache_pages = file_mod.default_read_cache_capacity,
1531     });
1532     defer workspace.deallocate(allocator);
1533 
1534     try overwriteTestingRefs(tmp.dir, stale);
1535     var successful = TestingControl{};
1536     try std.testing.expectEqual(
1537         std.meta.Tag(ReadRefsRefreshResult).refreshed,
1538         std.meta.activeTag(try refreshReadRefs(
1539             allocator,
1540             &workspace,
1541             tmp.dir,
1542             testingReadOptions(),
1543             successful.control(),
1544         )),
1545     );
1546     try std.testing.expect(successful.calls > 0);
1547 
1548     try overwriteTestingRefs(tmp.dir, stale);
1549     var interrupted = TestingControl{ .interrupt_at = successful.calls - 1 };
1550     try std.testing.expectError(
1551         error.Interrupted,
1552         refreshReadRefs(
1553             allocator,
1554             &workspace,
1555             tmp.dir,
1556             testingReadOptions(),
1557             interrupted.control(),
1558         ),
1559     );
1560     const after = try tmp.dir.readFileAlloc(
1561         testing_io,
1562         "live.history.refs",
1563         allocator,
1564         .unlimited,
1565     );
1566     defer allocator.free(after);
1567     try std.testing.expectEqualSlices(u8, stale, after);
1568 }
1569 
1570 test "lifecycle moved history preserves stale refs before retry publication" {
1571     const allocator = std.testing.allocator;
1572     var tmp = std.testing.tmpDir(.{});
1573     defer tmp.cleanup();
1574     const options = testingOptions();
1575     {
1576         var opened = try openForTesting(allocator, tmp.dir, options);
1577         defer opened.deinit();
1578         _ = try commitTestingRow(&opened, allocator, "CREATE TABLE items (name)");
1579     }
1580     const stale = try tmp.dir.readFileAlloc(
1581         testing_io,
1582         "live.history.refs",
1583         allocator,
1584         .unlimited,
1585     );
1586     defer allocator.free(stale);
1587     {
1588         var opened = try openForTesting(allocator, tmp.dir, options);
1589         defer opened.deinit();
1590         _ = try commitTestingRow(
1591             &opened,
1592             allocator,
1593             "INSERT INTO items (rowid, name) VALUES (1, 'later')",
1594         );
1595     }
1596 
1597     var history = try history_mod.History.open(allocator, tmp.dir, .{
1598         .io = testing_io,
1599         .path = options.history_path,
1600         .recovery = .reject,
1601     });
1602     defer history.deinit();
1603     try overwriteTestingRefs(tmp.dir, stale);
1604     const history_before = try tmp.dir.statFile(testing_io, options.history_path, .{});
1605     var workspace = try Workspace.allocate(allocator, .{
1606         .header = options.header,
1607         .max_wal_bytes = options.max_wal_bytes,
1608         .path_storage = .{ .database_bytes = 0, .wal_bytes = 0 },
1609         .read_cache_pages = file_mod.default_read_cache_capacity,
1610     });
1611     defer workspace.deallocate(allocator);
1612     const moved_root = version.DatabaseRoot.init(&.{.{
1613         .name = "unreferenced",
1614         .hash = version.emptyHash("lifecycle.moved-history"),
1615     }}, version.ConflictRoot.empty());
1616     var move = TestingHistoryMove{
1617         .history = &history,
1618         .root = moved_root,
1619         .move_at = 1,
1620     };
1621 
1622     try std.testing.expectEqual(
1623         std.meta.Tag(ReadRefsRefreshResult).moved,
1624         std.meta.activeTag(try refreshReadRefs(
1625             allocator,
1626             &workspace,
1627             tmp.dir,
1628             testingReadOptions(),
1629             move.control(),
1630         )),
1631     );
1632     try std.testing.expect(move.moved);
1633     try std.testing.expect(!move.failed);
1634     const history_after = try tmp.dir.statFile(testing_io, options.history_path, .{});
1635     try std.testing.expect(history_after.size > history_before.size);
1636     const after_move = try tmp.dir.readFileAlloc(
1637         testing_io,
1638         "live.history.refs",
1639         allocator,
1640         .unlimited,
1641     );
1642     defer allocator.free(after_move);
1643     try std.testing.expectEqualSlices(u8, stale, after_move);
1644 
1645     try std.testing.expectEqual(
1646         std.meta.Tag(ReadRefsRefreshResult).refreshed,
1647         std.meta.activeTag(try refreshReadRefs(
1648             allocator,
1649             &workspace,
1650             tmp.dir,
1651             testingReadOptions(),
1652             .{},
1653         )),
1654     );
1655     var repaired = (try history_mod.refs.loadExisting(
1656         allocator,
1657         testing_io,
1658         tmp.dir,
1659         options.history_path,
1660     )).?;
1661     defer repaired.deinit();
1662     try std.testing.expectEqual(history_after.size, repaired.covered_length);
1663 }
1664 
1665 test "lifecycle read-only classifies branch root and freshness repairs" {
1666     const allocator = std.testing.allocator;
1667     var tmp = std.testing.tmpDir(.{});
1668     defer tmp.cleanup();
1669     _ = try createTestingReadState(allocator, tmp.dir);
1670 
1671     var missing_branch = testingReadOptions();
1672     missing_branch.branch = "missing";
1673     try expectReadRepairTag(tmp.dir, missing_branch, .missing_branch);
1674 
1675     var snapshot = (try history_mod.refs.load(
1676         allocator,
1677         testing_io,
1678         tmp.dir,
1679         "live.history",
1680     )) orelse return error.SnapshotMissing;
1681     defer snapshot.deinit();
1682     var changed = false;
1683     for (snapshot.entries) |*entry| {
1684         if (!std.mem.eql(u8, entry.name, "main")) continue;
1685         entry.root = version.emptyHash("read-only root mismatch");
1686         changed = true;
1687         break;
1688     }
1689     try std.testing.expect(changed);
1690     try history_mod.refs.store(
1691         allocator,
1692         testing_io,
1693         tmp.dir,
1694         "live.history",
1695         snapshot.covered_length,
1696         snapshot.entries,
1697     );
1698     try expectReadRepairTag(tmp.dir, testingReadOptions(), .root_mismatch);
1699 
1700     try history_mod.refs.store(
1701         allocator,
1702         testing_io,
1703         tmp.dir,
1704         "live.history",
1705         snapshot.covered_length + 1,
1706         snapshot.entries,
1707     );
1708     try expectReadRepairTag(tmp.dir, testingReadOptions(), .stale_refs);
1709 }
1710 
1711 test "lifecycle read-only classifies missing and invalid refs" {
1712     const allocator = std.testing.allocator;
1713     var tmp = std.testing.tmpDir(.{});
1714     defer tmp.cleanup();
1715     _ = try createTestingReadState(allocator, tmp.dir);
1716     const refs_path = "live.history" ++ history_mod.refs.suffix;
1717 
1718     try tmp.dir.deleteFile(testing_io, refs_path);
1719     try expectReadRepairTag(tmp.dir, testingReadOptions(), .missing_refs);
1720     {
1721         var refs_file = try tmp.dir.createFile(testing_io, refs_path, .{
1722             .read = true,
1723             .truncate = true,
1724         });
1725         defer refs_file.close(testing_io);
1726         try refs_file.writePositionalAll(testing_io, "invalid refs", 0);
1727     }
1728     try expectReadRepairTag(tmp.dir, testingReadOptions(), .invalid_refs);
1729 }
1730 
1731 test "lifecycle read-only propagates refs allocation failure" {
1732     var tmp = std.testing.tmpDir(.{});
1733     defer tmp.cleanup();
1734     _ = try createTestingReadState(std.testing.allocator, tmp.dir);
1735     var failing = std.testing.FailingAllocator.init(
1736         std.testing.allocator,
1737         .{ .fail_index = 0 },
1738     );
1739     try std.testing.expectError(
1740         error.OutOfMemory,
1741         openReadOnlyForTesting(failing.allocator(), tmp.dir, testingReadOptions()),
1742     );
1743 }
1744 
1745 test "lifecycle read-only maps file repair reasons" {
1746     const allocator = std.testing.allocator;
1747     var tmp = std.testing.tmpDir(.{});
1748     defer tmp.cleanup();
1749     _ = try createTestingReadState(allocator, tmp.dir);
1750 
1751     try tmp.dir.deleteFile(testing_io, "live.db");
1752     try tmp.dir.deleteFile(testing_io, "live.wal");
1753     const reason = try readRepairReason(tmp.dir, testingReadOptions());
1754     switch (reason) {
1755         .file => |file_reason| try std.testing.expectEqual(
1756             file_mod.ReadRepairReason.missing_snapshot,
1757             file_reason,
1758         ),
1759         else => return error.ExpectedFileRepairRequired,
1760     }
1761 }
1762 
1763 test "lifecycle first open creates and later opens defer history replay" {
1764     const allocator = std.testing.allocator;
1765     var tmp = std.testing.tmpDir(.{});
1766     defer tmp.cleanup();
1767 
1768     {
1769         var opened = try openForTesting(allocator, tmp.dir, testingOptions());
1770         defer opened.deinit();
1771         try std.testing.expectEqual(Recovery.created, opened.recovery);
1772         try std.testing.expect(opened.history.replayed());
1773         var created = try opened.connection.execute(allocator, "CREATE TABLE items (name)", .{ .durability = .buffered });
1774         created.deinit(allocator);
1775         _ = try commit(&opened.connection, &opened.history, opened.file);
1776     }
1777 
1778     var reopened = try openForTesting(allocator, tmp.dir, testingOptions());
1779     defer reopened.deinit();
1780     try std.testing.expectEqual(Recovery.clean, reopened.recovery);
1781     try std.testing.expect(!reopened.history.replayed());
1782     const checkout = try reopened.history.checkoutBranch("main");
1783     try std.testing.expectEqualStrings("main", checkout.name);
1784     try std.testing.expect(!(try reopened.connection.checkout()).working.dirty());
1785 }
1786 
1787 test "lifecycle deferred history upgrades once for a commit and refreshes the sidecar" {
1788     const allocator = std.testing.allocator;
1789     var tmp = std.testing.tmpDir(.{});
1790     defer tmp.cleanup();
1791 
1792     {
1793         var opened = try openForTesting(allocator, tmp.dir, testingOptions());
1794         defer opened.deinit();
1795         var created = try opened.connection.execute(allocator, "CREATE TABLE items (name)", .{ .durability = .buffered });
1796         created.deinit(allocator);
1797         _ = try commit(&opened.connection, &opened.history, opened.file);
1798     }
1799 
1800     var first_head: version.Hash = undefined;
1801     {
1802         var opened = try openForTesting(allocator, tmp.dir, testingOptions());
1803         defer opened.deinit();
1804         try std.testing.expect(!opened.history.replayed());
1805         first_head = try commitTestingRow(&opened, allocator, "INSERT INTO items (rowid, name) VALUES (1, 'alpha')");
1806         try std.testing.expect(opened.history.replayed());
1807     }
1808 
1809     var reopened = try openForTesting(allocator, tmp.dir, testingOptions());
1810     defer reopened.deinit();
1811     try std.testing.expect(!reopened.history.replayed());
1812     const checkout = try reopened.history.checkoutBranch("main");
1813     try std.testing.expect(version.same(checkout.head, first_head));
1814     var rows = try reopened.connection.execute(allocator, "SELECT name FROM items", .{});
1815     defer rows.deinit(allocator);
1816     try std.testing.expectEqual(@as(usize, 1), rows.rowCount());
1817 }
1818 
1819 test "lifecycle truncated fast forward decision repairs the accepted prepare" {
1820     const allocator = std.testing.allocator;
1821     var tmp = std.testing.tmpDir(.{});
1822     defer tmp.cleanup();
1823 
1824     var baseline: version.Hash = undefined;
1825     var target: version.Hash = undefined;
1826     var coordinator_length: usize = 0;
1827     {
1828         var opened = try openForTesting(allocator, tmp.dir, testingOptions());
1829         defer opened.deinit();
1830         _ = try commitTestingRow(&opened, allocator, "CREATE TABLE items (name)");
1831         baseline = try commitTestingRow(
1832             &opened,
1833             allocator,
1834             "INSERT INTO items (rowid, name) VALUES (1, 'baseline')",
1835         );
1836         _ = try opened.connection.createBranch(try opened.history.full(), "target");
1837         try alignToBranchHead(
1838             allocator,
1839             &opened.connection,
1840             &opened.history,
1841             opened.file,
1842             "target",
1843         );
1844         target = try commitTestingRow(
1845             &opened,
1846             allocator,
1847             "INSERT INTO items (rowid, name) VALUES (2, 'target')",
1848         );
1849         try alignToBranchHead(
1850             allocator,
1851             &opened.connection,
1852             &opened.history,
1853             opened.file,
1854             "main",
1855         );
1856         const history = try opened.history.full();
1857         var update = try history.beginFastForward("main", baseline, target);
1858         try update.commit();
1859         coordinator_length = history.bytes_written;
1860     }
1861 
1862     var primary = try tmp.dir.openFile(testing_io, "live.history", .{ .mode = .read_write });
1863     try primary.setLength(testing_io, coordinator_length - 1);
1864     try primary.sync(testing_io);
1865     primary.close(testing_io);
1866 
1867     var repaired = try openForTesting(allocator, tmp.dir, testingOptions());
1868     defer repaired.deinit();
1869     try std.testing.expectEqual(Recovery.truncated_history, repaired.recovery);
1870     try std.testing.expect(repaired.history.replayed());
1871     try std.testing.expect((try repaired.history.full()).fastForwardRecovery() == null);
1872     try std.testing.expect(version.same(baseline, (try repaired.connection.checkout()).head));
1873     try std.testing.expect(version.same(
1874         baseline,
1875         (try (try repaired.history.full()).ref("main")).?.target,
1876     ));
1877     var rows = try repaired.connection.execute(allocator, "SELECT name FROM items", .{});
1878     defer rows.deinit(allocator);
1879     try std.testing.expectEqual(@as(usize, 1), rows.rowCount());
1880 }
1881 
1882 test "lifecycle reopen restores a committed conflict root" {
1883     const allocator = std.testing.allocator;
1884     var tmp = std.testing.tmpDir(.{});
1885     defer tmp.cleanup();
1886     const expected = try createTestingConflictHistory(allocator, tmp.dir);
1887     var reopened = try openForTesting(allocator, tmp.dir, testingOptions());
1888     defer reopened.deinit();
1889     try std.testing.expectEqual(Recovery.clean, reopened.recovery);
1890     try std.testing.expect(!reopened.history.replayed());
1891     try std.testing.expect(version.same(expected.commit, (try reopened.connection.checkout()).head));
1892     try std.testing.expect(version.same(expected.database, (try reopened.connection.workingRoot())));
1893     try std.testing.expect(!(try reopened.connection.checkout()).working.dirty());
1894     try std.testing.expect(version.same(expected.conflicts, reopened.connection.session.workingRoot().conflicts));
1895     var artifacts = try reopened.connection.conflictArtifacts(
1896         allocator,
1897         try reopened.history.full(),
1898     );
1899     defer artifacts.deinit();
1900     try std.testing.expectEqual(@as(usize, 1), artifacts.artifacts.len);
1901     try std.testing.expect(version.same(expected.artifact, artifacts.artifacts[0].hash));
1902 }
1903 
1904 test "lifecycle stale deferred conflict identity falls back and heals" {
1905     const allocator = std.testing.allocator;
1906     var tmp = std.testing.tmpDir(.{});
1907     defer tmp.cleanup();
1908     _ = try createTestingConflictHistory(allocator, tmp.dir);
1909 
1910     var snapshot = (try history_mod.refs.load(
1911         allocator,
1912         testing_io,
1913         tmp.dir,
1914         "live.history",
1915     )) orelse return error.SnapshotMissing;
1916     defer snapshot.deinit();
1917     var changed = false;
1918     for (snapshot.entries) |*entry| {
1919         if (!std.mem.eql(u8, entry.name, "main")) continue;
1920         entry.conflicts = version.emptyHash("stale deferred conflicts");
1921         changed = true;
1922         break;
1923     }
1924     try std.testing.expect(changed);
1925     try history_mod.refs.store(
1926         allocator,
1927         testing_io,
1928         tmp.dir,
1929         "live.history",
1930         snapshot.covered_length,
1931         snapshot.entries,
1932     );
1933 
1934     {
1935         var opened = try openForTesting(allocator, tmp.dir, testingOptions());
1936         defer opened.deinit();
1937         try std.testing.expect(opened.history.replayed());
1938     }
1939     var healed = try openForTesting(allocator, tmp.dir, testingOptions());
1940     defer healed.deinit();
1941     try std.testing.expect(!healed.history.replayed());
1942 }
1943 
1944 test "lifecycle missing or stale sidecar falls back to full replay and heals" {
1945     const allocator = std.testing.allocator;
1946     var tmp = std.testing.tmpDir(.{});
1947     defer tmp.cleanup();
1948 
1949     {
1950         var opened = try openForTesting(allocator, tmp.dir, testingOptions());
1951         defer opened.deinit();
1952         var created = try opened.connection.execute(allocator, "CREATE TABLE items (name)", .{ .durability = .buffered });
1953         created.deinit(allocator);
1954         _ = try commit(&opened.connection, &opened.history, opened.file);
1955     }
1956 
1957     try tmp.dir.deleteFile(testing_io, "live.history" ++ history_mod.refs.suffix);
1958     {
1959         var opened = try openForTesting(allocator, tmp.dir, testingOptions());
1960         defer opened.deinit();
1961         try std.testing.expectEqual(Recovery.clean, opened.recovery);
1962         try std.testing.expect(opened.history.replayed());
1963     }
1964 
1965     var healed = try openForTesting(allocator, tmp.dir, testingOptions());
1966     defer healed.deinit();
1967     try std.testing.expect(!healed.history.replayed());
1968 }
1969 
1970 test "lifecycle deferred open reads through the working database without history bytes" {
1971     const allocator = std.testing.allocator;
1972     var tmp = std.testing.tmpDir(.{});
1973     defer tmp.cleanup();
1974 
1975     {
1976         var opened = try openForTesting(allocator, tmp.dir, testingOptions());
1977         defer opened.deinit();
1978         var created = try opened.connection.execute(allocator, "CREATE TABLE items (name)", .{ .durability = .buffered });
1979         created.deinit(allocator);
1980         var inserted = try opened.connection.execute(allocator, "INSERT INTO items (rowid, name) VALUES (1, 'alpha')", .{ .durability = .buffered });
1981         inserted.deinit(allocator);
1982         _ = try commit(&opened.connection, &opened.history, opened.file);
1983     }
1984 
1985     {
1986         var scribbled = try tmp.dir.openFile(testing_io, "live.history", .{ .mode = .read_write });
1987         defer scribbled.close(testing_io);
1988         const length = try scribbled.length(testing_io);
1989         try std.testing.expect(length > 8);
1990         try scribbled.writePositionalAll(testing_io, "XXXXXXXX", length / 2);
1991     }
1992 
1993     var opened = try openForTesting(allocator, tmp.dir, testingOptions());
1994     defer opened.deinit();
1995     try std.testing.expect(!opened.history.replayed());
1996     var rows = try opened.connection.execute(allocator, "SELECT name FROM items", .{});
1997     defer rows.deinit(allocator);
1998     try std.testing.expectEqual(@as(usize, 1), rows.rowCount());
1999 }
2000 
2001 test "lifecycle commit heals a corrupted history from the working database" {
2002     const allocator = std.testing.allocator;
2003     var tmp = std.testing.tmpDir(.{});
2004     defer tmp.cleanup();
2005 
2006     {
2007         var opened = try openForTesting(allocator, tmp.dir, testingOptions());
2008         defer opened.deinit();
2009         var created = try opened.connection.execute(allocator, "CREATE TABLE items (name)", .{ .durability = .buffered });
2010         created.deinit(allocator);
2011         var inserted = try opened.connection.execute(allocator, "INSERT INTO items (rowid, name) VALUES (1, 'alpha')", .{ .durability = .buffered });
2012         inserted.deinit(allocator);
2013         _ = try commit(&opened.connection, &opened.history, opened.file);
2014     }
2015 
2016     {
2017         var scribbled = try tmp.dir.openFile(testing_io, "live.history", .{ .mode = .read_write });
2018         defer scribbled.close(testing_io);
2019         const length = try scribbled.length(testing_io);
2020         try scribbled.writePositionalAll(testing_io, "XXXXXXXX", length / 2);
2021     }
2022 
2023     {
2024         var opened = try openForTesting(allocator, tmp.dir, testingOptions());
2025         defer opened.deinit();
2026         try std.testing.expect(!opened.history.replayed());
2027         var inserted = try opened.connection.execute(allocator, "INSERT INTO items (rowid, name) VALUES (2, 'beta')", .{ .durability = .buffered });
2028         inserted.deinit(allocator);
2029         _ = try commit(&opened.connection, &opened.history, opened.file);
2030         try std.testing.expect(opened.history.replayed());
2031     }
2032 
2033     var reopened = try openForTesting(allocator, tmp.dir, testingOptions());
2034     defer reopened.deinit();
2035     try std.testing.expectEqual(Recovery.clean, reopened.recovery);
2036     var rows = try reopened.connection.execute(allocator, "SELECT name FROM items", .{});
2037     defer rows.deinit(allocator);
2038     try std.testing.expectEqual(@as(usize, 2), rows.rowCount());
2039 }