Skip to documentation
SLOP

tiny.sql.lifecycle

Reference tiny.sql lifecycle

Defined in tiny.sql.

API (33)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callstiny.sqllifecycle
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsNo direct callerslifecyclecloselifecycle.Openeddeinit
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallsNo direct callsprivate sourcelib.sql.src.lifecyclecreateTestingConflictHistorytest sourcelib.sql.src.lifecycletest: lifecycle truncated fast forwar...lifecyclealignToBranchHead
Static calls · unresolved targets: 0 · external targets: 3.
Called byCallsNo direct callslifecyclecheckpointIfWalLargelifecyclecheckpoint
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callerslifecyclecheckpointlifecyclewalSizelifecyclecheckpointIfWalLarge
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallslifecycle.Openeddeinitprivate sourcelib.sql.src.lifecycle.WorkspaceownerOflifecycleclose
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallsprivate sourcelib.sql.src.lifecyclecommitTestingRowtest sourcelib.sql.src.lifecycletest: lifecycle commit heals a corrup...test sourcelib.sql.src.lifecycletest: lifecycle deferred history upgr...test sourcelib.sql.src.lifecycletest: lifecycle deferred open reads t...test sourcelib.sql.src.lifecycletest: lifecycle first open creates an...test sourcelib.sql.src.lifecycletest: lifecycle missing or stale side...private sourcelib.sql.src.lifecyclerebuiltHistorylifecyclecommit
Static calls · unresolved targets: 1 · external targets: 5.
Called byCallsNo direct callsprivate sourcelib.sql.src.lifecyclecreateTestingConflictHistorylifecyclemergeCommit
Static calls · unresolved targets: 0 · external targets: 3.
Called byCallslifecycleopenForTestingtest sourcelib.sql.src.lifecycletest: lifecycle workspace reuses one ...private sourcelib.sql.src.lifecycledeferredCheckoutprivate sourcelib.sql.src.lifecyclefileExistsprivate sourcelib.sql.src.lifecycleliveConnectionprivate sourcelib.sql.src.lifecyclereplayedConnectionversionsamelifecycleopen
Static calls · unresolved targets: 1 · external targets: 12.
Called byCallstest sourcelib.sql.src.lifecycletest: lifecycle read connection deriv...BranchCheckoutinitprivate sourcelib.sql.src.lifecycleliveConnectionprivate sourcelib.sql.src.lifecycleloadReadRefsversionsamelifecycleopenConnectionReadOnly
Static calls · unresolved targets: 1 · external targets: 8.
Called byCallsprivate sourcelib.sql.src.lifecyclecreateTestingConflictHistoryprivate sourcelib.sql.src.lifecyclecreateTestingReadStatetest sourcelib.sql.src.lifecycletest: lifecycle commit heals a corrup...test sourcelib.sql.src.lifecycletest: lifecycle deferred history upgr...test sourcelib.sql.src.lifecycletest: lifecycle deferred open reads t...+8 moreLifecycleWorkspaceallocatelifecycleopenlifecycleopenForTesting
Static calls · unresolved targets: 0 · external targets: 4.
Called byCallslifecycleopenReadOnlyForTestingprivate sourcelib.sql.src.lifecycleloadReadRefsprivate sourcelib.sql.src.lifecycleopenReadOnlyFromSnapshotlifecycleopenReadOnly
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsprivate sourcelib.sql.src.lifecyclereadRepairReasontest sourcelib.sql.src.lifecycletest: lifecycle read-only forwards re...test sourcelib.sql.src.lifecycletest: lifecycle read-only open owns a...test sourcelib.sql.src.lifecycletest: lifecycle read-only propagates ...LifecycleWorkspaceallocatelifecycleopenReadOnlylifecycleopenReadOnlyForTesting
Static calls · unresolved targets: 0 · external targets: 3.
Called byCallsprivate sourcelib.sql.src.lifecyclerefreshReadRefsprivate sourcelib.sql.src.lifecycleprepareStaleReadRefsprivate sourcelib.sql.src.lifecyclevalidateReadRefslifecycleprepareReadRefs
Static calls · unresolved targets: 0 · external targets: 5.
Called byCallsNo direct callslifecyclecheckpointIfWalLargelifecyclewalSize
Static calls · unresolved targets: 0 · external targets: 1.

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.

Audit

Definitions21
Public names21
Members22
Version26.7.0
Revisiondaab053ee433