tiny.sql.lifecycle
Defined in tiny.sql.
API (33)
Actions
Public operations.
Opened.deinitalignToBranchHeadcheckpointcheckpointIfWalLargeclosecommitfastForwardBranchmergeCommitopenopenConnectionReadOnlyopenForTestingopenReadOnlyopenReadOnlyForTestingprepareReadRefswalSize
Types and contracts
Public types and contracts.
ConnectionReadOpenResultErrorLazyHistoryOpenedOptionsReadOpenResultReadOpenedReadOptionsReadRefsPrepareResultReadRefsRefreshResultReadRepairReasonRecoverySparseReadAssessmentSparseReadBeginResultSparseReadFinishResultSparseReadOptionsSparseReadSessionWorkspace: Owns one database and the storage that database runs on, created by a tool at startup to open its database through it.
Source
Source: lib/sql/src/lifecycle.zig
zig
const std = @import("std");const builtin = @import("builtin");const branch = @import("branch.zig");const catalog_mod = @import("catalog.zig");const connection_mod = @import("connection.zig");const file_mod = @import("file.zig");const history_mod = @import("history/root.zig");const pager = @import("pager.zig");const session_mod = @import("session/root.zig");const version = @import("version.zig");const wal = @import("wal.zig");const Allocator = std.mem.Allocator;const default_capacity: pager.Capacity = .{ .wal_frames = 8192 };/// Owns one database and the storage that database runs on, created by a tool/// at startup to open its database through it. The workspace holds the file/// workspace and, once opened, the database itself, and hands callers a pointer/// to that database. Because callers hold that pointer, the workspace has to/// stay at one address from the open until the opened database is released, so/// callers place it on the heap. The workspace takes its storage either from/// the caller with `init` or from an allocator with `allocate`, and gives it/// back with the matching `deinit` or `deallocate`. The workspace opens one/// database at a time, reports `WorkspaceBusy` for a second open while the/// first is live, and refuses teardown by assertion while a database is still/// open.pub const Workspace = struct { pub const Limits = file_mod.Database.Workspace.Limits; pub const Capacity = file_mod.Database.Workspace.Capacity; pub const Storage = file_mod.Database.Workspace.Storage; pub const InitError = file_mod.Database.Workspace.InitError; pub const AllocateError = file_mod.Database.Workspace.AllocateError; pub const AcquireError = file_mod.Database.Workspace.AcquireError; file_workspace: file_mod.Database.Workspace, database: file_mod.Database = undefined, database_live: bool = false, pub fn init( storage: Storage, limits: Limits, ) InitError!Workspace { return .{ .file_workspace = try file_mod.Database.Workspace.init(storage, limits), }; } pub fn allocate(allocator: Allocator, limits: Limits) AllocateError!Workspace { return .{ .file_workspace = try file_mod.Database.Workspace.allocate(allocator, limits), }; } pub fn activate(self: *Workspace) void { std.debug.assert(!self.database_live); self.file_workspace.activate(); } pub fn deinit(self: *Workspace) Storage { std.debug.assert(!self.database_live); const storage = self.file_workspace.deinit(); self.* = undefined; return storage; } pub fn deallocate(self: *Workspace, allocator: Allocator) void { std.debug.assert(!self.database_live); self.file_workspace.deallocate(allocator); self.* = undefined; } pub fn retainedBytes(self: *const Workspace) usize { return self.file_workspace.retainedBytes(); } fn acquireWritable( self: *Workspace, allocator: Allocator, dir: std.Io.Dir, options: file_mod.OpenOptions, ) file_mod.Error!*file_mod.Database { if (self.database_live) return error.WorkspaceBusy; const writable = try file_mod.Database.open( allocator, &self.file_workspace, dir, options, ); self.database = writable; self.database_live = true; return &self.database; } fn acquireReadOnly( self: *Workspace, allocator: Allocator, dir: std.Io.Dir, options: file_mod.ReadOpenOptions, ) file_mod.Error!*file_mod.Database { if (self.database_live) return error.WorkspaceBusy; self.database = try file_mod.Database.openReadOnly( allocator, &self.file_workspace, dir, options, ); self.database_live = true; return &self.database; } fn releaseDatabase(self: *Workspace, database: *file_mod.Database) void { std.debug.assert(self.database_live); std.debug.assert(database == &self.database); database.deinit(); self.database_live = false; } fn ownerOf(writable: *file_mod.Database) *Workspace { const owner: *Workspace = @alignCast(@fieldParentPtr( "file_workspace", writable.workspace, )); std.debug.assert(owner.database_live); std.debug.assert(writable == &owner.database); return owner; }};pub const Error = file_mod.Error || history_mod.Error || history_mod.refs.ReadError || connection_mod.Error || std.Io.Dir.DeleteFileError;pub const Options = struct { io: std.Io = std.Options.debug_io, paths: file_mod.Paths = .{}, history_path: []const u8, branch: []const u8 = "main", header: wal.Header, max_wal_bytes: usize = file_mod.default_max_wal_bytes, wal_capacity_bytes: ?usize = null, capacity: pager.Capacity = default_capacity, control: wal.Control = .{},};pub const ReadOptions = struct { io: std.Io = std.Options.debug_io, paths: file_mod.Paths = .{}, history_path: []const u8, branch: []const u8 = "main", header: wal.Header, max_wal_bytes: usize = file_mod.default_max_wal_bytes, read_cache_capacity: usize = file_mod.default_read_cache_capacity, advance_refs: bool = false,};pub const Recovery = enum { clean, created, truncated_history, rebuilt_invalid_history, rebuilt_missing_branch,};pub const LazyHistory = struct { allocator: Allocator, io: std.Io, dir: std.Io.Dir, path: []u8, state: State, retired: ?history_mod.refs.Snapshot = null, read_only: bool = false, const State = union(enum) { deferred: history_mod.refs.Snapshot, open: history_mod.History, }; pub fn full(self: *LazyHistory) Error!*history_mod.History { switch (self.state) { .open => {}, .deferred => |snapshot| { std.debug.assert(self.retired == null); const opened = try history_mod.History.open(self.allocator, self.dir, .{ .io = self.io, .path = self.path, .recovery = .reject, .create = !self.read_only, .read_only = self.read_only, }); self.retired = snapshot; self.state = .{ .open = opened }; }, } return &self.state.open; } pub fn replayed(self: *const LazyHistory) bool { return self.state == .open; } pub fn checkoutBranch(self: *const LazyHistory, name: []const u8) Error!branch.Checkout { return switch (self.state) { .open => |*history| try history.checkoutBranch(name), .deferred => |*snapshot| checkout: { const entry = snapshot.find(name) orelse return error.RefNotFound; break :checkout branch.Checkout.init( .{ .name = entry.name, .target = entry.head }, entry.root, ); }, }; } pub fn deinit(self: *LazyHistory) void { switch (self.state) { .open => |*history| history.deinit(), .deferred => |*snapshot| snapshot.deinit(), } if (self.retired) |*snapshot| snapshot.deinit(); self.dir.close(self.io); self.allocator.free(self.path); self.* = undefined; }};pub const Opened = struct { allocator: Allocator, workspace: *Workspace, workspace_owned: bool = false, file: *file_mod.Database, history: LazyHistory, connection: connection_mod.Connection, recovery: Recovery, pub fn deinit(self: *Opened) void { const allocator = self.allocator; const workspace = self.workspace; const workspace_owned = self.workspace_owned; close(self.file, &self.history, &self.connection); if (workspace_owned) { workspace.deallocate(allocator); allocator.destroy(workspace); } self.* = undefined; }};pub const ReadRepairReason = union(enum) { file: file_mod.ReadRepairReason, missing_refs, invalid_refs, stale_refs, missing_branch, root_mismatch,};pub const ReadOpened = struct { allocator: Allocator, workspace: *Workspace, workspace_owned: bool = false, file: *file_mod.ReadOnlyDatabase, catalog: catalog_mod.Reader, head: version.Hash, root: version.Hash, pub fn deinit(self: *ReadOpened) void { const allocator = self.allocator; const workspace = self.workspace; const workspace_owned = self.workspace_owned; self.file.deinit(); self.allocator.destroy(self.file); if (workspace_owned) { workspace.deallocate(allocator); allocator.destroy(workspace); } self.* = undefined; }};pub const ReadOpenResult = union(enum) { ready: ReadOpened, repair_required: ReadRepairReason,};pub const ConnectionReadOpenResult = union(enum) { ready: Opened, repair_required: ReadRepairReason,};pub const ReadRefsRefreshResult = union(enum) { current, refreshed, moved, repair_required: ReadRepairReason,};pub const ReadRefsPrepareResult = union(enum) { current, candidate: history_mod.refs.Snapshot, moved, repair_required: ReadRepairReason,};pub const SparseReadOptions = struct { read: ReadOptions, control: wal.Control = .{},};const SparseBranch = struct { covered_length: u64, head: version.Hash, root: version.Hash, conflicts: version.Hash,};pub const SparseReadAssessment = struct { catalog: catalog_mod.Reader, head: version.Hash, root: version.Hash, branch: SparseBranch, live_root: version.Hash,};pub const SparseReadBeginResult = union(enum) { ready: SparseReadAssessment, moved, repair_required: ReadRepairReason,};pub const SparseReadFinishResult = union(enum) { stable, moved, repair_required: ReadRepairReason,};const SparseRefreshResult = union(enum) { ready: std.meta.Tag(file_mod.SparseRefreshResult), repair_required: ReadRepairReason,};pub const SparseReadSession = struct { workspace: wal.Scanner.Workspace, file: file_mod.SparseReadOnlyDatabase = undefined, file_live: bool = false, pub fn init(self: *SparseReadSession, workspace: wal.Scanner.Workspace) void { self.* = .{ .workspace = workspace }; } pub fn deinit(self: *SparseReadSession) void { self.resetFile(); self.* = undefined; } pub fn begin( self: *SparseReadSession, allocator: Allocator, dir: std.Io.Dir, options: SparseReadOptions, ) Error!SparseReadBeginResult { try options.control.check(); const branch_state = switch (try loadSparseBranch(allocator, dir, options.read, options.control)) { .ready => |ready| ready, .repair_required => |reason| return .{ .repair_required = reason }, }; switch (try self.refreshFile(dir, options)) { .ready => {}, .repair_required => |reason| return .{ .repair_required = reason }, } try options.control.check(); const catalog = catalog_mod.Reader.open(self.file.snapshot(), .{}) catch |err| { if (err == error.RecoveryRequired) { self.resetFile(); return .moved; } return err; }; var live_root = version.readDatabaseRootMaintained( allocator, &catalog, branch_state.conflicts, ) catch |err| { if (err == error.RecoveryRequired) { self.resetFile(); return .moved; } return err; }; defer live_root.deinit(); try options.control.check(); return .{ .ready = .{ .catalog = catalog, .head = branch_state.head, .root = branch_state.root, .branch = branch_state, .live_root = live_root.hash, } }; } pub fn finish( self: *SparseReadSession, allocator: Allocator, dir: std.Io.Dir, assessment: SparseReadAssessment, options: SparseReadOptions, ) Error!SparseReadFinishResult { try options.control.check(); const file_moved = switch (try self.refreshFile(dir, options)) { .ready => |state| state == .refreshed, .repair_required => |reason| return .{ .repair_required = reason }, }; const branch_state = switch (try loadSparseBranch(allocator, dir, options.read, options.control)) { .ready => |ready| ready, .repair_required => |reason| return .{ .repair_required = reason }, }; const catalog = catalog_mod.Reader.open(self.file.snapshot(), .{}) catch |err| { if (err == error.RecoveryRequired) { self.resetFile(); return .moved; } return err; }; var live_root = version.readDatabaseRootMaintained( allocator, &catalog, branch_state.conflicts, ) catch |err| { if (err == error.RecoveryRequired) { self.resetFile(); return .moved; } return err; }; defer live_root.deinit(); try options.control.check(); if (file_moved or !std.meta.eql(branch_state, assessment.branch) or !version.same(live_root.hash, assessment.live_root)) { return .moved; } if (!version.same(live_root.hash, branch_state.root)) { return .{ .repair_required = .root_mismatch }; } return .stable; } fn refreshFile( self: *SparseReadSession, dir: std.Io.Dir, options: SparseReadOptions, ) Error!SparseRefreshResult { if (!self.file_live) { const opened = file_mod.SparseReadOnlyDatabase.openExisting(dir, .{ .io = options.read.io, .paths = options.read.paths, .workspace = self.workspace, .control = options.control, }) catch |err| { self.resetFile(); return err; }; switch (opened) { .ready => |ready| { self.file = ready; self.file_live = true; return .{ .ready = .refreshed }; }, .repair_required => |reason| return .{ .repair_required = .{ .file = reason }, }, } } const refreshed = self.file.refresh(dir, .{ .paths = options.read.paths, .control = options.control, }) catch |err| { self.resetFile(); return err; }; return switch (refreshed) { .unchanged => .{ .ready = .unchanged }, .refreshed => .{ .ready = .refreshed }, .repair_required => |reason| repair: { self.resetFile(); break :repair .{ .repair_required = .{ .file = reason } }; }, }; } fn resetFile(self: *SparseReadSession) void { if (self.file_live) self.file.deinit(); self.file_live = false; }};pub fn open( allocator: Allocator, workspace: *Workspace, dir: std.Io.Dir, options: Options,) Error!Opened { var recovery: Recovery = if (fileExists(options.io, dir, options.history_path)) .clean else .created; const file_ptr = try workspace.acquireWritable(allocator, dir, .{ .io = options.io, .paths = options.paths, .header = options.header, .max_wal_bytes = options.max_wal_bytes, .wal_capacity_bytes = options.wal_capacity_bytes, .write_capacity = options.capacity, .control = options.control, }); errdefer workspace.releaseDatabase(file_ptr); var owned_dir = try dir.openDir(options.io, ".", .{}); errdefer owned_dir.close(options.io); const owned_path = try allocator.dupe(u8, options.history_path); errdefer allocator.free(owned_path); if (recovery == .clean) { if (try deferredCheckout(allocator, dir, options)) |deferred| { var snapshot = deferred.snapshot; errdefer snapshot.deinit(); var connection = try liveConnection( allocator, file_ptr, deferred.checkout, deferred.conflicts, ); if (version.same((try connection.workingRoot()), deferred.checkout.working.working)) { return .{ .allocator = allocator, .workspace = workspace, .file = file_ptr, .history = .{ .allocator = allocator, .io = options.io, .dir = owned_dir, .path = owned_path, .state = .{ .deferred = snapshot }, }, .connection = connection, .recovery = .clean, }; } connection.deinit(); snapshot.deinit(); } } var history = history_mod.History.open(allocator, dir, .{ .io = options.io, .path = options.history_path, .recovery = .truncate, .control = options.control, }) catch |err| switch (err) { error.InvalidHistory => blk: { recovery = .rebuilt_invalid_history; try dir.deleteFile(options.io, options.history_path); break :blk try history_mod.History.open(allocator, dir, .{ .io = options.io, .path = options.history_path, .recovery = .truncate, .control = options.control, }); }, else => return err, }; errdefer history.deinit(); if (history.recovery == .truncated) recovery = .truncated_history; var connection = switch (recovery) { .created, .rebuilt_invalid_history => try connection_mod.Connection.create( allocator, file_ptr, &history, .{ .branch = options.branch }, ), .clean, .truncated_history, .rebuilt_missing_branch => replayedConnection( allocator, file_ptr, &history, options.branch, ) catch |err| switch (err) { error.RefNotFound => blk: { recovery = .rebuilt_missing_branch; break :blk try connection_mod.Connection.create(allocator, file_ptr, &history, .{ .branch = options.branch }); }, else => return err, }, }; errdefer connection.deinit(); return .{ .allocator = allocator, .workspace = workspace, .file = file_ptr, .history = .{ .allocator = allocator, .io = options.io, .dir = owned_dir, .path = owned_path, .state = .{ .open = history }, }, .connection = connection, .recovery = recovery, };}pub fn openForTesting(allocator: Allocator, dir: std.Io.Dir, options: Options) Error!Opened { if (!builtin.is_test) @compileError("openForTesting is available only in tests"); const workspace = try allocator.create(Workspace); errdefer allocator.destroy(workspace); workspace.* = try Workspace.allocate(allocator, .{ .header = options.header, .max_wal_bytes = options.wal_capacity_bytes orelse options.max_wal_bytes, .path_storage = file_mod.PathStorage.Limits.forDirect(options.paths), }); errdefer workspace.deallocate(allocator); var opened = try open(allocator, workspace, dir, options); opened.workspace_owned = true; return opened;}pub fn openReadOnly( allocator: Allocator, workspace: *Workspace, dir: std.Io.Dir, options: ReadOptions,) Error!ReadOpenResult { var refs_snapshot = switch (try loadReadRefs(allocator, dir, options, .{})) { .ready => |ready| ready, .repair_required => |reason| return .{ .repair_required = reason }, }; defer refs_snapshot.deinit(); return try openReadOnlyFromSnapshot( allocator, workspace, dir, options, &refs_snapshot, .{}, );}pub fn openConnectionReadOnly( allocator: Allocator, workspace: *Workspace, dir: std.Io.Dir, options: ReadOptions,) Error!ConnectionReadOpenResult { var snapshot = switch (try loadReadRefs(allocator, dir, options, .{})) { .ready => |ready| ready, .repair_required => |reason| return .{ .repair_required = reason }, }; var snapshot_live = true; defer if (snapshot_live) snapshot.deinit(); const entry = snapshot.find(options.branch) orelse return .{ .repair_required = .missing_branch }; const file_ptr = try workspace.acquireReadOnly(allocator, dir, .{ .io = options.io, .paths = options.paths, .header = options.header, .max_wal_bytes = options.max_wal_bytes, .read_cache_capacity = options.read_cache_capacity, }); var file_live = true; defer if (file_live) workspace.releaseDatabase(file_ptr); var connection = try liveConnection( allocator, file_ptr, branch.Checkout.init(.{ .name = entry.name, .target = entry.head }, entry.root), entry.conflicts, ); var connection_live = true; defer if (connection_live) connection.deinit(); if (!version.same(try connection.workingRoot(), entry.root)) { return .{ .repair_required = .root_mismatch }; } var owned_dir = try dir.openDir(options.io, ".", .{}); errdefer owned_dir.close(options.io); const owned_path = try allocator.dupe(u8, options.history_path); snapshot_live = false; file_live = false; connection_live = false; return .{ .ready = .{ .allocator = allocator, .workspace = workspace, .file = file_ptr, .history = .{ .allocator = allocator, .io = options.io, .dir = owned_dir, .path = owned_path, .state = .{ .deferred = snapshot }, .read_only = true, }, .connection = connection, .recovery = .clean, } };}fn openReadOnlyFromSnapshot( allocator: Allocator, workspace: *Workspace, dir: std.Io.Dir, options: ReadOptions, refs_snapshot: *const history_mod.refs.Snapshot, control: wal.Control,) Error!ReadOpenResult { try control.check(); const entry = refs_snapshot.find(options.branch) orelse return .{ .repair_required = .missing_branch }; var database_value = switch (try file_mod.ReadOnlyDatabase.openExisting( allocator, &workspace.file_workspace, dir, .{ .io = options.io, .paths = options.paths, .header = options.header, .max_wal_bytes = options.max_wal_bytes, .read_cache_capacity = options.read_cache_capacity, .control = control, }, )) { .ready => |ready| ready, .repair_required => |reason| return .{ .repair_required = .{ .file = reason } }, }; const database = allocator.create(file_mod.ReadOnlyDatabase) catch |err| { database_value.deinit(); return err; }; database.* = database_value; var database_live = true; defer if (database_live) { database.deinit(); allocator.destroy(database); }; try control.check(); const catalog = try catalog_mod.Reader.open(database.snapshot(), .{}); var live_root = try version.readDatabaseRootMaintained(allocator, &catalog, entry.conflicts); defer live_root.deinit(); try control.check(); if (!version.same(live_root.hash, entry.root)) { return .{ .repair_required = .root_mismatch }; } database_live = false; return .{ .ready = .{ .allocator = allocator, .workspace = workspace, .file = database, .catalog = catalog, .head = entry.head, .root = entry.root, } };}pub fn prepareReadRefs( allocator: Allocator, workspace: *Workspace, dir: std.Io.Dir, options: ReadOptions, control: wal.Control,) Error!ReadRefsPrepareResult { try control.check(); var path_buffer: [history_mod.refs.path_bytes_max]u8 = undefined; if (history_mod.refs.pathFor(&path_buffer, options.history_path) == null) { return .{ .repair_required = .invalid_refs }; } var snapshot = (history_mod.refs.loadExisting( allocator, options.io, dir, options.history_path, ) catch |err| switch (err) { error.FileNotFound => return .{ .repair_required = .missing_refs }, else => return err, }) orelse return .{ .repair_required = .invalid_refs }; defer snapshot.deinit(); const history_stat = dir.statFile( options.io, options.history_path, .{}, ) catch |err| switch (err) { error.FileNotFound => return .{ .repair_required = .stale_refs }, else => return err, }; if (snapshot.covered_length == history_stat.size) { const reason = try validateReadRefs( allocator, workspace, dir, options, &snapshot, control, ); if (reason) |value| return .{ .repair_required = value }; try control.check(); const final_stat = dir.statFile(options.io, options.history_path, .{}) catch return .moved; return if (final_stat.size == history_stat.size) .current else .moved; } if (snapshot.covered_length > history_stat.size) { return .{ .repair_required = .stale_refs }; } return prepareStaleReadRefs( allocator, workspace, dir, options, &snapshot, history_stat.size, control, );}fn prepareStaleReadRefs( allocator: Allocator, workspace: *Workspace, dir: std.Io.Dir, options: ReadOptions, snapshot: *const history_mod.refs.Snapshot, history_length: u64, control: wal.Control,) Error!ReadRefsPrepareResult { var candidate = history_mod.refs.advance( allocator, options.io, dir, options.history_path, snapshot, history_length, control, ) catch |err| switch (err) { error.InvalidHistory, error.TruncatedHistory, error.RecoveryRequired, error.StreamTooLong, => return .{ .repair_required = .stale_refs }, else => return err, }; var candidate_live = true; defer if (candidate_live) candidate.deinit(); if (try validateReadRefs( allocator, workspace, dir, options, &candidate, control, )) |reason| return .{ .repair_required = reason }; const final_stat = dir.statFile(options.io, options.history_path, .{}) catch return .moved; if (final_stat.size != history_length) return .moved; try control.check(); candidate_live = false; return .{ .candidate = candidate };}fn refreshReadRefs( allocator: Allocator, workspace: *Workspace, dir: std.Io.Dir, options: ReadOptions, control: wal.Control,) Error!ReadRefsRefreshResult { if (!builtin.is_test) @compileError("read refs publication is tracker-owned"); return switch (try prepareReadRefs( allocator, workspace, dir, options, control, )) { .current => .current, .candidate => |candidate_value| publish: { var candidate = candidate_value; defer candidate.deinit(); try history_mod.refs.store( allocator, options.io, dir, options.history_path, candidate.covered_length, candidate.entries, ); break :publish .refreshed; }, .moved => .moved, .repair_required => |reason| .{ .repair_required = reason }, };}fn validateReadRefs( allocator: Allocator, workspace: *Workspace, dir: std.Io.Dir, options: ReadOptions, snapshot: *const history_mod.refs.Snapshot, control: wal.Control,) Error!?ReadRepairReason { return switch (try openReadOnlyFromSnapshot( allocator, workspace, dir, options, snapshot, control, )) { .ready => |ready_value| ready: { var ready = ready_value; ready.deinit(); break :ready null; }, .repair_required => |reason| reason, };}pub fn openReadOnlyForTesting( allocator: Allocator, dir: std.Io.Dir, options: ReadOptions,) Error!ReadOpenResult { if (!builtin.is_test) @compileError("openReadOnlyForTesting is available only in tests"); const workspace = try allocator.create(Workspace); errdefer allocator.destroy(workspace); workspace.* = try Workspace.allocate(allocator, .{ .header = options.header, .max_wal_bytes = options.max_wal_bytes, .path_storage = .{ .database_bytes = 0, .wal_bytes = 0, }, .read_cache_pages = options.read_cache_capacity, }); errdefer workspace.deallocate(allocator); const result = try openReadOnly(allocator, workspace, dir, options); return switch (result) { .ready => |ready| opened: { var value = ready; value.workspace_owned = true; break :opened .{ .ready = value }; }, .repair_required => |reason| repair: { workspace.deallocate(allocator); allocator.destroy(workspace); break :repair .{ .repair_required = reason }; }, };}pub fn close( file_ptr: *file_mod.Database, history: *LazyHistory, connection: *connection_mod.Connection,) void { connection.deinit(); history.deinit(); Workspace.ownerOf(file_ptr).releaseDatabase(file_ptr);}pub fn commit( connection: *connection_mod.Connection, history: *LazyHistory, file_ptr: *file_mod.Database,) Error!version.Hash { var healed = false; const replay = history.full() catch |err| switch (err) { error.InvalidHistory, error.TruncatedHistory => blk: { healed = true; break :blk try rebuiltHistory(connection, history); }, else => return err, }; try connection.stage(); const head = connection.commit(replay) catch |err| switch (err) { error.NoStagedRoot => head: { if (!healed) return err; break :head (try connection.checkout()).head; }, else => return err, }; try file_ptr.syncWal(); return head;}fn rebuiltHistory( connection: *connection_mod.Connection, history: *LazyHistory,) Error!*history_mod.History { std.debug.assert(!history.replayed()); history.dir.deleteFile(history.io, history.path) catch |err| switch (err) { error.FileNotFound => {}, else => return err, }; const branch_name = (try connection.checkout()).name; const fresh = try history_mod.History.open(history.allocator, history.dir, .{ .io = history.io, .path = history.path, .recovery = .reject, }); const snapshot = history.state.deferred; history.state = .{ .open = fresh }; std.debug.assert(history.retired == null); history.retired = snapshot; const opened = &history.state.open; try connection.adoptRebuiltHistory(opened, branch_name); return opened;}pub fn mergeCommit( connection: *connection_mod.Connection, history: *LazyHistory, file_ptr: *file_mod.Database, theirs: version.Hash,) Error!version.Hash { const replay = try history.full(); const head = try connection.mergeCommit(replay, theirs); try file_ptr.syncWal(); return head;}pub fn alignToBranchHead( allocator: Allocator, connection: *connection_mod.Connection, history: *LazyHistory, file_ptr: *file_mod.Database, branch_name: []const u8,) Error!void { try connection.checkoutBranch(allocator, try history.full(), branch_name); try file_ptr.syncWal();}pub fn fastForwardBranch( allocator: Allocator, connection: *connection_mod.Connection, history: *LazyHistory, target: version.Hash,) Error!void { try connection.fastForwardBranch(allocator, try history.full(), target);}pub fn walSize(file_ptr: *const file_mod.Database) usize { return file_ptr.pager.walBytes().len;}pub fn checkpoint(file_ptr: *file_mod.Database, header: wal.Header) Error!void { _ = try file_ptr.checkpoint(.{ .restart_header = header });}pub fn checkpointIfWalLarge(file_ptr: *file_mod.Database, header: wal.Header, threshold: usize) Error!void { if (walSize(file_ptr) >= threshold) try checkpoint(file_ptr, header);}const DeferredCheckout = struct { snapshot: history_mod.refs.Snapshot, checkout: branch.Checkout, conflicts: version.Hash,};const ReadRefsResult = union(enum) { ready: history_mod.refs.Snapshot, repair_required: ReadRepairReason,};const SparseBranchResult = union(enum) { ready: SparseBranch, repair_required: ReadRepairReason,};fn loadSparseBranch( allocator: Allocator, dir: std.Io.Dir, options: ReadOptions, control: wal.Control,) Error!SparseBranchResult { var snapshot = switch (try loadReadRefs(allocator, dir, options, control)) { .ready => |ready| ready, .repair_required => |reason| return .{ .repair_required = reason }, }; defer snapshot.deinit(); const entry = snapshot.find(options.branch) orelse return .{ .repair_required = .missing_branch }; return .{ .ready = .{ .covered_length = snapshot.covered_length, .head = entry.head, .root = entry.root, .conflicts = entry.conflicts, } };}fn loadReadRefs( allocator: Allocator, dir: std.Io.Dir, options: ReadOptions, control: wal.Control,) Error!ReadRefsResult { try control.check(); var path_buffer: [history_mod.refs.path_bytes_max]u8 = undefined; if (history_mod.refs.pathFor(&path_buffer, options.history_path) == null) { return .{ .repair_required = .invalid_refs }; } var snapshot = (history_mod.refs.loadExisting( allocator, options.io, dir, options.history_path, ) catch |err| switch (err) { error.FileNotFound => return .{ .repair_required = .missing_refs }, else => return err, }) orelse return .{ .repair_required = .invalid_refs }; var snapshot_live = true; defer if (snapshot_live) snapshot.deinit(); const history_stat = dir.statFile( options.io, options.history_path, .{}, ) catch |err| switch (err) { error.FileNotFound => return .{ .repair_required = .stale_refs }, else => return err, }; if (snapshot.covered_length != history_stat.size) { if (!options.advance_refs or snapshot.covered_length > history_stat.size) { return .{ .repair_required = .stale_refs }; } const advanced = history_mod.refs.advance( allocator, options.io, dir, options.history_path, &snapshot, history_stat.size, control, ) catch |err| switch (err) { error.InvalidHistory, error.TruncatedHistory, error.RecoveryRequired, error.StreamTooLong, => return .{ .repair_required = .stale_refs }, else => return err, }; snapshot.deinit(); snapshot = advanced; try control.check(); const final_stat = try dir.statFile(options.io, options.history_path, .{}); if (final_stat.size != history_stat.size) { return .{ .repair_required = .stale_refs }; } } snapshot_live = false; return .{ .ready = snapshot };}fn deferredCheckout(allocator: Allocator, dir: std.Io.Dir, options: Options) Error!?DeferredCheckout { var snapshot = (try history_mod.refs.load(allocator, options.io, dir, options.history_path)) orelse return null; var snapshot_live = true; defer if (snapshot_live) snapshot.deinit(); const stat = dir.statFile(options.io, options.history_path, .{}) catch return null; if (snapshot.covered_length != stat.size) return null; const entry = snapshot.find(options.branch) orelse return null; snapshot_live = false; return .{ .snapshot = snapshot, .checkout = branch.Checkout.init(.{ .name = entry.name, .target = entry.head }, entry.root), .conflicts = entry.conflicts, };}fn liveConnection( allocator: Allocator, file_ptr: *file_mod.Database, checkout: branch.Checkout, conflicts: version.Hash,) Error!connection_mod.Connection { const catalog = try catalog_mod.Catalog.open(file_ptr, .{}); var root = try version.databaseRootMaintained(allocator, &catalog, conflicts); errdefer root.deinit(); const live_checkout = checkout.withWorking(root.hash); const session = session_mod.DatabaseSession.initWithRoot(allocator, live_checkout, &root); return connection_mod.Connection.init(catalog, session);}fn replayedConnection( allocator: Allocator, file_ptr: *file_mod.Database, history: *history_mod.History, branch_name: []const u8,) Error!connection_mod.Connection { return try connection_mod.Connection.open(allocator, file_ptr, history, .{ .branch = branch_name, });}fn fileExists(io: std.Io, dir: std.Io.Dir, path: []const u8) bool { _ = dir.statFile(io, path, .{}) catch return false; return true;}const testing_io = std.Options.debug_io;fn testingHeader() wal.Header { return .{ .sequence = 91, .salt = .{ .first = 0x0101_2323, .second = 0x4545_6767 }, };}fn testingOptions() Options { return .{ .io = testing_io, .paths = .{ .database = "live.db", .wal = "live.wal" }, .history_path = "live.history", .header = testingHeader(), };}fn testingReadOptions() ReadOptions { return .{ .io = testing_io, .paths = .{ .database = "live.db", .wal = "live.wal" }, .history_path = "live.history", .header = testingHeader(), };}fn commitTestingRow(opened: *Opened, allocator: Allocator, statement: []const u8) !version.Hash { var result = try opened.connection.execute(allocator, statement, .{ .durability = .buffered }); result.deinit(allocator); return try commit(&opened.connection, &opened.history, opened.file);}const TestingReadState = struct { head: version.Hash, root: version.Hash,};const TestingControl = struct { calls: usize = 0, interrupt_at: ?usize = null, fn control(self: *TestingControl) wal.Control { return .{ .context = self, .interrupted_fn = interrupted }; } fn interrupted(context: ?*anyopaque) bool { const self: *TestingControl = @ptrCast(@alignCast(context.?)); const call = self.calls; self.calls += 1; return if (self.interrupt_at) |interrupt_at| call == interrupt_at else false; }};const TestingHistoryMove = struct { history: *history_mod.History, root: version.DatabaseRoot, move_at: usize, calls: usize = 0, moved: bool = false, failed: bool = false, fn control(self: *TestingHistoryMove) wal.Control { return .{ .context = self, .interrupted_fn = interrupted }; } fn interrupted(context: ?*anyopaque) bool { const self: *TestingHistoryMove = @ptrCast(@alignCast(context.?)); const call = self.calls; self.calls += 1; if (call == self.move_at) { self.history.putDatabaseRoot(self.root) catch { self.failed = true; return false; }; self.moved = true; } return false; }};fn overwriteTestingRefs(dir: std.Io.Dir, bytes: []const u8) !void { var file = try dir.createFile( testing_io, "live.history.refs", .{ .read = true, .truncate = true }, ); defer file.close(testing_io); try file.writePositionalAll(testing_io, bytes, 0);}fn createTestingReadState(allocator: Allocator, dir: std.Io.Dir) !TestingReadState { var opened = try openForTesting(allocator, dir, testingOptions()); defer opened.deinit(); const head = try commitTestingRow(&opened, allocator, "CREATE TABLE items (name)"); return .{ .head = head, .root = try opened.connection.workingRoot() };}fn readRepairReason(dir: std.Io.Dir, options: ReadOptions) !ReadRepairReason { return switch (try openReadOnlyForTesting(std.testing.allocator, dir, options)) { .repair_required => |reason| reason, .ready => |ready_value| { var ready = ready_value; ready.deinit(); return error.ExpectedRepairRequired; }, };}fn expectReadRepairTag( dir: std.Io.Dir, options: ReadOptions, expected: std.meta.Tag(ReadRepairReason),) !void { try std.testing.expectEqual(expected, std.meta.activeTag(try readRepairReason(dir, options)));}const TestingConflictHistory = struct { commit: version.Hash, database: version.Hash, conflicts: version.Hash, artifact: version.Hash,};fn createTestingConflictHistory(allocator: Allocator, dir: std.Io.Dir) !TestingConflictHistory { var opened = try openForTesting(allocator, dir, testingOptions()); defer opened.deinit(); _ = try commitTestingRow(&opened, allocator, "CREATE TABLE items (name)"); _ = try commitTestingRow( &opened, allocator, "INSERT INTO items (rowid, name) VALUES (1, 'base')", ); _ = try opened.connection.createBranch(try opened.history.full(), "side"); _ = try commitTestingRow( &opened, allocator, "UPDATE items SET name = 'ours' WHERE rowid = 1", ); try alignToBranchHead(allocator, &opened.connection, &opened.history, opened.file, "side"); const side = try commitTestingRow( &opened, allocator, "UPDATE items SET name = 'theirs' WHERE rowid = 1", ); try alignToBranchHead(allocator, &opened.connection, &opened.history, opened.file, "main"); var merged = try opened.connection.mergeBranch( allocator, try opened.history.full(), "side", .{}, ); defer merged.deinit(); return .{ .commit = try mergeCommit(&opened.connection, &opened.history, opened.file, side), .database = try opened.connection.workingRoot(), .conflicts = merged.conflict_root.hash, .artifact = merged.artifacts[0].hash, };}test "lifecycle workspace reuses one pinned writable slot" { const allocator = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const options = testingOptions(); var workspace = try Workspace.allocate(allocator, .{ .header = options.header, .max_wal_bytes = options.wal_capacity_bytes orelse options.max_wal_bytes, .path_storage = file_mod.PathStorage.Limits.forDirect(options.paths), }); defer workspace.deallocate(allocator); var first = try open(allocator, &workspace, tmp.dir, options); const slot = first.file; try std.testing.expectEqual(&workspace.database, slot); try std.testing.expectError( error.WorkspaceBusy, open(allocator, &workspace, tmp.dir, options), ); first.deinit(); var second = try open(allocator, &workspace, tmp.dir, options); defer second.deinit(); try std.testing.expectEqual(slot, second.file);}test "lifecycle read-only open owns a stable maintained snapshot" { const allocator = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const expected = try createTestingReadState(allocator, tmp.dir); var opened = switch (try openReadOnlyForTesting(allocator, tmp.dir, testingReadOptions())) { .ready => |ready| ready, .repair_required => return error.UnexpectedRepairRequired, }; defer opened.deinit(); try std.testing.expect(version.same(expected.head, opened.head)); try std.testing.expect(version.same(expected.root, opened.root)); var names = try opened.catalog.relationNames(allocator); defer names.deinit(); try std.testing.expectEqual(@as(usize, 1), names.names.len); try std.testing.expectEqualSlices(u8, "items", names.names[0]);}test "lifecycle read connection derives stale refs and rejects durable writes" { const allocator = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); _ = try createTestingReadState(allocator, tmp.dir); const stale = try tmp.dir.readFileAlloc( testing_io, "live.history.refs", allocator, .limited(1 << 20), ); defer allocator.free(stale); var expected: version.Hash = undefined; { var writer = try openForTesting(allocator, tmp.dir, testingOptions()); defer writer.deinit(); expected = try commitTestingRow( &writer, allocator, "INSERT INTO items (rowid, name) VALUES (2, 'later')", ); } try overwriteTestingRefs(tmp.dir, stale); const before = try readArtifactMetadata(tmp.dir); var options = testingReadOptions(); options.advance_refs = true; var workspace = try Workspace.allocate(allocator, .{ .header = options.header, .max_wal_bytes = options.max_wal_bytes, .path_storage = file_mod.PathStorage.Limits.forDirect(options.paths), }); defer workspace.deallocate(allocator); { var opened = switch (try openConnectionReadOnly(allocator, &workspace, tmp.dir, options)) { .ready => |ready| ready, .repair_required => return error.UnexpectedRepairRequired, }; defer opened.deinit(); try std.testing.expect(version.same(expected, opened.connection.session.checkout.head)); const history = try opened.history.full(); try std.testing.expect(version.same(expected, (try history.checkoutBranch("main")).head)); try std.testing.expectError(error.ReadOnly, history.createBranch("forbidden", expected)); try std.testing.expectError(error.ReadOnlyDatabase, opened.connection.execute( allocator, "INSERT INTO items (rowid, name) VALUES (3, 'forbidden')", .{}, )); } try std.testing.expectEqualDeep(before, try readArtifactMetadata(tmp.dir)); const after = try tmp.dir.readFileAlloc( testing_io, "live.history.refs", allocator, .limited(1 << 20), ); defer allocator.free(after); try std.testing.expectEqualSlices(u8, stale, after);}const ReadArtifactMetadata = struct { size: u64, mtime: std.Io.Timestamp, ctime: std.Io.Timestamp,};fn readArtifactMetadata(dir: std.Io.Dir) ![4]ReadArtifactMetadata { const paths = [_][]const u8{ "live.db", "live.wal", "live.history", "live.history.refs", }; var values: [paths.len]ReadArtifactMetadata = undefined; for (paths, &values) |path, *value| { const stat = try dir.statFile(testing_io, path, .{}); value.* = .{ .size = stat.size, .mtime = stat.mtime, .ctime = stat.ctime }; } return values;}test "lifecycle read-only forwards read cache capacity" { const allocator = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); _ = try createTestingReadState(allocator, tmp.dir); var options = testingReadOptions(); options.read_cache_capacity = 3; var opened = switch (try openReadOnlyForTesting(allocator, tmp.dir, options)) { .ready => |ready| ready, .repair_required => return error.UnexpectedRepairRequired, }; defer opened.deinit(); try std.testing.expectEqual(@as(usize, 3), opened.file.readCacheCapacity());}test "lifecycle late read refs cancellation preserves the stale sidecar" { const allocator = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const options = testingOptions(); { var opened = try openForTesting(allocator, tmp.dir, options); defer opened.deinit(); _ = try commitTestingRow(&opened, allocator, "CREATE TABLE items (name)"); } const stale = try tmp.dir.readFileAlloc( testing_io, "live.history.refs", allocator, .unlimited, ); defer allocator.free(stale); { var opened = try openForTesting(allocator, tmp.dir, options); defer opened.deinit(); _ = try commitTestingRow( &opened, allocator, "INSERT INTO items (rowid, name) VALUES (1, 'later')", ); } var workspace = try Workspace.allocate(allocator, .{ .header = options.header, .max_wal_bytes = options.max_wal_bytes, .path_storage = .{ .database_bytes = 0, .wal_bytes = 0 }, .read_cache_pages = file_mod.default_read_cache_capacity, }); defer workspace.deallocate(allocator); try overwriteTestingRefs(tmp.dir, stale); var successful = TestingControl{}; try std.testing.expectEqual( std.meta.Tag(ReadRefsRefreshResult).refreshed, std.meta.activeTag(try refreshReadRefs( allocator, &workspace, tmp.dir, testingReadOptions(), successful.control(), )), ); try std.testing.expect(successful.calls > 0); try overwriteTestingRefs(tmp.dir, stale); var interrupted = TestingControl{ .interrupt_at = successful.calls - 1 }; try std.testing.expectError( error.Interrupted, refreshReadRefs( allocator, &workspace, tmp.dir, testingReadOptions(), interrupted.control(), ), ); const after = try tmp.dir.readFileAlloc( testing_io, "live.history.refs", allocator, .unlimited, ); defer allocator.free(after); try std.testing.expectEqualSlices(u8, stale, after);}test "lifecycle moved history preserves stale refs before retry publication" { const allocator = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const options = testingOptions(); { var opened = try openForTesting(allocator, tmp.dir, options); defer opened.deinit(); _ = try commitTestingRow(&opened, allocator, "CREATE TABLE items (name)"); } const stale = try tmp.dir.readFileAlloc( testing_io, "live.history.refs", allocator, .unlimited, ); defer allocator.free(stale); { var opened = try openForTesting(allocator, tmp.dir, options); defer opened.deinit(); _ = try commitTestingRow( &opened, allocator, "INSERT INTO items (rowid, name) VALUES (1, 'later')", ); } var history = try history_mod.History.open(allocator, tmp.dir, .{ .io = testing_io, .path = options.history_path, .recovery = .reject, }); defer history.deinit(); try overwriteTestingRefs(tmp.dir, stale); const history_before = try tmp.dir.statFile(testing_io, options.history_path, .{}); var workspace = try Workspace.allocate(allocator, .{ .header = options.header, .max_wal_bytes = options.max_wal_bytes, .path_storage = .{ .database_bytes = 0, .wal_bytes = 0 }, .read_cache_pages = file_mod.default_read_cache_capacity, }); defer workspace.deallocate(allocator); const moved_root = version.DatabaseRoot.init(&.{.{ .name = "unreferenced", .hash = version.emptyHash("lifecycle.moved-history"), }}, version.ConflictRoot.empty()); var move = TestingHistoryMove{ .history = &history, .root = moved_root, .move_at = 1, }; try std.testing.expectEqual( std.meta.Tag(ReadRefsRefreshResult).moved, std.meta.activeTag(try refreshReadRefs( allocator, &workspace, tmp.dir, testingReadOptions(), move.control(), )), ); try std.testing.expect(move.moved); try std.testing.expect(!move.failed); const history_after = try tmp.dir.statFile(testing_io, options.history_path, .{}); try std.testing.expect(history_after.size > history_before.size); const after_move = try tmp.dir.readFileAlloc( testing_io, "live.history.refs", allocator, .unlimited, ); defer allocator.free(after_move); try std.testing.expectEqualSlices(u8, stale, after_move); try std.testing.expectEqual( std.meta.Tag(ReadRefsRefreshResult).refreshed, std.meta.activeTag(try refreshReadRefs( allocator, &workspace, tmp.dir, testingReadOptions(), .{}, )), ); var repaired = (try history_mod.refs.loadExisting( allocator, testing_io, tmp.dir, options.history_path, )).?; defer repaired.deinit(); try std.testing.expectEqual(history_after.size, repaired.covered_length);}test "lifecycle read-only classifies branch root and freshness repairs" { const allocator = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); _ = try createTestingReadState(allocator, tmp.dir); var missing_branch = testingReadOptions(); missing_branch.branch = "missing"; try expectReadRepairTag(tmp.dir, missing_branch, .missing_branch); var snapshot = (try history_mod.refs.load( allocator, testing_io, tmp.dir, "live.history", )) orelse return error.SnapshotMissing; defer snapshot.deinit(); var changed = false; for (snapshot.entries) |*entry| { if (!std.mem.eql(u8, entry.name, "main")) continue; entry.root = version.emptyHash("read-only root mismatch"); changed = true; break; } try std.testing.expect(changed); try history_mod.refs.store( allocator, testing_io, tmp.dir, "live.history", snapshot.covered_length, snapshot.entries, ); try expectReadRepairTag(tmp.dir, testingReadOptions(), .root_mismatch); try history_mod.refs.store( allocator, testing_io, tmp.dir, "live.history", snapshot.covered_length + 1, snapshot.entries, ); try expectReadRepairTag(tmp.dir, testingReadOptions(), .stale_refs);}test "lifecycle read-only classifies missing and invalid refs" { const allocator = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); _ = try createTestingReadState(allocator, tmp.dir); const refs_path = "live.history" ++ history_mod.refs.suffix; try tmp.dir.deleteFile(testing_io, refs_path); try expectReadRepairTag(tmp.dir, testingReadOptions(), .missing_refs); { var refs_file = try tmp.dir.createFile(testing_io, refs_path, .{ .read = true, .truncate = true, }); defer refs_file.close(testing_io); try refs_file.writePositionalAll(testing_io, "invalid refs", 0); } try expectReadRepairTag(tmp.dir, testingReadOptions(), .invalid_refs);}test "lifecycle read-only propagates refs allocation failure" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); _ = try createTestingReadState(std.testing.allocator, tmp.dir); var failing = std.testing.FailingAllocator.init( std.testing.allocator, .{ .fail_index = 0 }, ); try std.testing.expectError( error.OutOfMemory, openReadOnlyForTesting(failing.allocator(), tmp.dir, testingReadOptions()), );}test "lifecycle read-only maps file repair reasons" { const allocator = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); _ = try createTestingReadState(allocator, tmp.dir); try tmp.dir.deleteFile(testing_io, "live.db"); try tmp.dir.deleteFile(testing_io, "live.wal"); const reason = try readRepairReason(tmp.dir, testingReadOptions()); switch (reason) { .file => |file_reason| try std.testing.expectEqual( file_mod.ReadRepairReason.missing_snapshot, file_reason, ), else => return error.ExpectedFileRepairRequired, }}test "lifecycle first open creates and later opens defer history replay" { const allocator = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); { var opened = try openForTesting(allocator, tmp.dir, testingOptions()); defer opened.deinit(); try std.testing.expectEqual(Recovery.created, opened.recovery); try std.testing.expect(opened.history.replayed()); var created = try opened.connection.execute(allocator, "CREATE TABLE items (name)", .{ .durability = .buffered }); created.deinit(allocator); _ = try commit(&opened.connection, &opened.history, opened.file); } var reopened = try openForTesting(allocator, tmp.dir, testingOptions()); defer reopened.deinit(); try std.testing.expectEqual(Recovery.clean, reopened.recovery); try std.testing.expect(!reopened.history.replayed()); const checkout = try reopened.history.checkoutBranch("main"); try std.testing.expectEqualStrings("main", checkout.name); try std.testing.expect(!(try reopened.connection.checkout()).working.dirty());}test "lifecycle deferred history upgrades once for a commit and refreshes the sidecar" { const allocator = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); { var opened = try openForTesting(allocator, tmp.dir, testingOptions()); defer opened.deinit(); var created = try opened.connection.execute(allocator, "CREATE TABLE items (name)", .{ .durability = .buffered }); created.deinit(allocator); _ = try commit(&opened.connection, &opened.history, opened.file); } var first_head: version.Hash = undefined; { var opened = try openForTesting(allocator, tmp.dir, testingOptions()); defer opened.deinit(); try std.testing.expect(!opened.history.replayed()); first_head = try commitTestingRow(&opened, allocator, "INSERT INTO items (rowid, name) VALUES (1, 'alpha')"); try std.testing.expect(opened.history.replayed()); } var reopened = try openForTesting(allocator, tmp.dir, testingOptions()); defer reopened.deinit(); try std.testing.expect(!reopened.history.replayed()); const checkout = try reopened.history.checkoutBranch("main"); try std.testing.expect(version.same(checkout.head, first_head)); var rows = try reopened.connection.execute(allocator, "SELECT name FROM items", .{}); defer rows.deinit(allocator); try std.testing.expectEqual(@as(usize, 1), rows.rowCount());}test "lifecycle truncated fast forward decision repairs the accepted prepare" { const allocator = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var baseline: version.Hash = undefined; var target: version.Hash = undefined; var coordinator_length: usize = 0; { var opened = try openForTesting(allocator, tmp.dir, testingOptions()); defer opened.deinit(); _ = try commitTestingRow(&opened, allocator, "CREATE TABLE items (name)"); baseline = try commitTestingRow( &opened, allocator, "INSERT INTO items (rowid, name) VALUES (1, 'baseline')", ); _ = try opened.connection.createBranch(try opened.history.full(), "target"); try alignToBranchHead( allocator, &opened.connection, &opened.history, opened.file, "target", ); target = try commitTestingRow( &opened, allocator, "INSERT INTO items (rowid, name) VALUES (2, 'target')", ); try alignToBranchHead( allocator, &opened.connection, &opened.history, opened.file, "main", ); const history = try opened.history.full(); var update = try history.beginFastForward("main", baseline, target); try update.commit(); coordinator_length = history.bytes_written; } var primary = try tmp.dir.openFile(testing_io, "live.history", .{ .mode = .read_write }); try primary.setLength(testing_io, coordinator_length - 1); try primary.sync(testing_io); primary.close(testing_io); var repaired = try openForTesting(allocator, tmp.dir, testingOptions()); defer repaired.deinit(); try std.testing.expectEqual(Recovery.truncated_history, repaired.recovery); try std.testing.expect(repaired.history.replayed()); try std.testing.expect((try repaired.history.full()).fastForwardRecovery() == null); try std.testing.expect(version.same(baseline, (try repaired.connection.checkout()).head)); try std.testing.expect(version.same( baseline, (try (try repaired.history.full()).ref("main")).?.target, )); var rows = try repaired.connection.execute(allocator, "SELECT name FROM items", .{}); defer rows.deinit(allocator); try std.testing.expectEqual(@as(usize, 1), rows.rowCount());}test "lifecycle reopen restores a committed conflict root" { const allocator = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const expected = try createTestingConflictHistory(allocator, tmp.dir); var reopened = try openForTesting(allocator, tmp.dir, testingOptions()); defer reopened.deinit(); try std.testing.expectEqual(Recovery.clean, reopened.recovery); try std.testing.expect(!reopened.history.replayed()); try std.testing.expect(version.same(expected.commit, (try reopened.connection.checkout()).head)); try std.testing.expect(version.same(expected.database, (try reopened.connection.workingRoot()))); try std.testing.expect(!(try reopened.connection.checkout()).working.dirty()); try std.testing.expect(version.same(expected.conflicts, reopened.connection.session.workingRoot().conflicts)); var artifacts = try reopened.connection.conflictArtifacts( allocator, try reopened.history.full(), ); defer artifacts.deinit(); try std.testing.expectEqual(@as(usize, 1), artifacts.artifacts.len); try std.testing.expect(version.same(expected.artifact, artifacts.artifacts[0].hash));}test "lifecycle stale deferred conflict identity falls back and heals" { const allocator = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); _ = try createTestingConflictHistory(allocator, tmp.dir); var snapshot = (try history_mod.refs.load( allocator, testing_io, tmp.dir, "live.history", )) orelse return error.SnapshotMissing; defer snapshot.deinit(); var changed = false; for (snapshot.entries) |*entry| { if (!std.mem.eql(u8, entry.name, "main")) continue; entry.conflicts = version.emptyHash("stale deferred conflicts"); changed = true; break; } try std.testing.expect(changed); try history_mod.refs.store( allocator, testing_io, tmp.dir, "live.history", snapshot.covered_length, snapshot.entries, ); { var opened = try openForTesting(allocator, tmp.dir, testingOptions()); defer opened.deinit(); try std.testing.expect(opened.history.replayed()); } var healed = try openForTesting(allocator, tmp.dir, testingOptions()); defer healed.deinit(); try std.testing.expect(!healed.history.replayed());}test "lifecycle missing or stale sidecar falls back to full replay and heals" { const allocator = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); { var opened = try openForTesting(allocator, tmp.dir, testingOptions()); defer opened.deinit(); var created = try opened.connection.execute(allocator, "CREATE TABLE items (name)", .{ .durability = .buffered }); created.deinit(allocator); _ = try commit(&opened.connection, &opened.history, opened.file); } try tmp.dir.deleteFile(testing_io, "live.history" ++ history_mod.refs.suffix); { var opened = try openForTesting(allocator, tmp.dir, testingOptions()); defer opened.deinit(); try std.testing.expectEqual(Recovery.clean, opened.recovery); try std.testing.expect(opened.history.replayed()); } var healed = try openForTesting(allocator, tmp.dir, testingOptions()); defer healed.deinit(); try std.testing.expect(!healed.history.replayed());}test "lifecycle deferred open reads through the working database without history bytes" { const allocator = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); { var opened = try openForTesting(allocator, tmp.dir, testingOptions()); defer opened.deinit(); var created = try opened.connection.execute(allocator, "CREATE TABLE items (name)", .{ .durability = .buffered }); created.deinit(allocator); var inserted = try opened.connection.execute(allocator, "INSERT INTO items (rowid, name) VALUES (1, 'alpha')", .{ .durability = .buffered }); inserted.deinit(allocator); _ = try commit(&opened.connection, &opened.history, opened.file); } { var scribbled = try tmp.dir.openFile(testing_io, "live.history", .{ .mode = .read_write }); defer scribbled.close(testing_io); const length = try scribbled.length(testing_io); try std.testing.expect(length > 8); try scribbled.writePositionalAll(testing_io, "XXXXXXXX", length / 2); } var opened = try openForTesting(allocator, tmp.dir, testingOptions()); defer opened.deinit(); try std.testing.expect(!opened.history.replayed()); var rows = try opened.connection.execute(allocator, "SELECT name FROM items", .{}); defer rows.deinit(allocator); try std.testing.expectEqual(@as(usize, 1), rows.rowCount());}test "lifecycle commit heals a corrupted history from the working database" { const allocator = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); { var opened = try openForTesting(allocator, tmp.dir, testingOptions()); defer opened.deinit(); var created = try opened.connection.execute(allocator, "CREATE TABLE items (name)", .{ .durability = .buffered }); created.deinit(allocator); var inserted = try opened.connection.execute(allocator, "INSERT INTO items (rowid, name) VALUES (1, 'alpha')", .{ .durability = .buffered }); inserted.deinit(allocator); _ = try commit(&opened.connection, &opened.history, opened.file); } { var scribbled = try tmp.dir.openFile(testing_io, "live.history", .{ .mode = .read_write }); defer scribbled.close(testing_io); const length = try scribbled.length(testing_io); try scribbled.writePositionalAll(testing_io, "XXXXXXXX", length / 2); } { var opened = try openForTesting(allocator, tmp.dir, testingOptions()); defer opened.deinit(); try std.testing.expect(!opened.history.replayed()); var inserted = try opened.connection.execute(allocator, "INSERT INTO items (rowid, name) VALUES (2, 'beta')", .{ .durability = .buffered }); inserted.deinit(allocator); _ = try commit(&opened.connection, &opened.history, opened.file); try std.testing.expect(opened.history.replayed()); } var reopened = try openForTesting(allocator, tmp.dir, testingOptions()); defer reopened.deinit(); try std.testing.expectEqual(Recovery.clean, reopened.recovery); var rows = try reopened.connection.execute(allocator, "SELECT name FROM items", .{}); defer rows.deinit(allocator); try std.testing.expectEqual(@as(usize, 2), rows.rowCount());}Source: lib/sql/src/root.zig:32
zig
pub const lifecycle = @import("lifecycle.zig");Complete caller list for lifecycle.openForTesting
13 direct callers.
lib.sql.src.lifecycle.createTestingConflictHistory[function] — private source atlib/sql/src/lifecycle.zig:1325in nearest public ownertiny.sql.lifecyclelib.sql.src.lifecycle.createTestingReadState[function] — private source atlib/sql/src/lifecycle.zig:1292in nearest public ownertiny.sql.lifecyclelib.sql.src.lifecycle.test_lifecycle_commit_heals_a_corrupted_history_from_the_working_database[function] — test source atlib/sql/src/lifecycle.zig:2001in nearest public ownertiny.sql.lifecyclelib.sql.src.lifecycle.test_lifecycle_deferred_history_upgrades_once_for_a_commit_and_refreshes_the_sidecar[function] — test source atlib/sql/src/lifecycle.zig:1787in nearest public ownertiny.sql.lifecyclelib.sql.src.lifecycle.test_lifecycle_deferred_open_reads_through_the_working_database_without_history_bytes[function] — test source atlib/sql/src/lifecycle.zig:1970in nearest public ownertiny.sql.lifecyclelib.sql.src.lifecycle.test_lifecycle_first_open_creates_and_later_opens_defer_history_replay[function] — test source atlib/sql/src/lifecycle.zig:1763in nearest public ownertiny.sql.lifecyclelib.sql.src.lifecycle.test_lifecycle_late_read_refs_cancellation_preserves_the_stale_sidecar[function] — test source atlib/sql/src/lifecycle.zig:1500in nearest public ownertiny.sql.lifecyclelib.sql.src.lifecycle.test_lifecycle_missing_or_stale_sidecar_falls_back_to_full_replay_and_heals[function] — test source atlib/sql/src/lifecycle.zig:1944in nearest public ownertiny.sql.lifecyclelib.sql.src.lifecycle.test_lifecycle_moved_history_preserves_stale_refs_before_retry_publication[function] — test source atlib/sql/src/lifecycle.zig:1570in nearest public ownertiny.sql.lifecyclelib.sql.src.lifecycle.test_lifecycle_read_connection_derives_stale_refs_and_rejects_durable_writes[function] — test source atlib/sql/src/lifecycle.zig:1407in nearest public ownertiny.sql.lifecyclelib.sql.src.lifecycle.test_lifecycle_reopen_restores_a_committed_conflict_root[function] — test source atlib/sql/src/lifecycle.zig:1882in nearest public ownertiny.sql.lifecyclelib.sql.src.lifecycle.test_lifecycle_stale_deferred_conflict_identity_falls_back_and_heals[function] — test source atlib/sql/src/lifecycle.zig:1904in nearest public ownertiny.sql.lifecyclelib.sql.src.lifecycle.test_lifecycle_truncated_fast_forward_decision_repairs_the_accepted_prepare[function] — test source atlib/sql/src/lifecycle.zig:1819in nearest public ownertiny.sql.lifecycle
Audit
| Definitions | 21 |
|---|---|
| Public names | 21 |
| Members | 22 |
| Version | 26.7.0 |
| Revision | daab053ee433 |