tiny.sql.connection
Defined in tiny.sql.
API (8)
Types and contracts
Public types and contracts.
Source
Source: lib/sql/src/connection.zig
zig
const std = @import("std");const simd = @import("simd");const branch_mod = @import("branch.zig");const catalog_mod = @import("catalog.zig");const diff_mod = @import("diff.zig");const file = @import("file.zig");const history_mod = @import("history/root.zig");const merge_mod = @import("merge.zig");const plan_mod = @import("plan.zig");const relation_mod = @import("relation.zig");const session_mod = @import("session/root.zig");const statement_mod = @import("statement/root.zig");const row = @import("row.zig");const tree = @import("tree.zig");const version = @import("version.zig");const wal = @import("wal.zig");const Bytes = simd.ScalableTag(u8);const Allocator = std.mem.Allocator;const ConnectionError = error{ ConflictNotFound, NoMergeBase, UnsupportedCheckoutRoot, WorkingSetChanged,};pub const Error = statement_mod.Error || history_mod.Error || diff_mod.Error || merge_mod.Error || version.Error || ConnectionError;pub const Options = struct { branch: []const u8 = "main", catalog: catalog_mod.Options = .{},};pub const ExecuteOptions = struct { durability: file.CommitDurability = .synced, validate_indexes: bool = false, write: ?*session_mod.DatabaseWrite = null, fn statement(self: ExecuteOptions, session: *session_mod.DatabaseSession) statement_mod.ExecuteOptions { return .{ .durability = self.durability, .validate_indexes = self.validate_indexes, .session = session, .write = self.write, }; }};pub const WriteOptions = struct { limits: session_mod.DatabaseWrite.Limits, durability: file.CommitDurability = .synced, validate_indexes: bool = false, fn commit(self: WriteOptions) file.CommitOptions { return .{ .durability = self.durability, .validate_indexes = self.validate_indexes, }; }};pub const MergeOptions = struct { commit: file.CommitOptions = .{ .durability = .buffered },};pub const RelationView = struct { allocator: Allocator, root: version.RelationRoot, handle: ?catalog_mod.RelationHandle = null, rows: []version.RelationRow = &.{}, pub fn snapshot(self: *const RelationView) diff_mod.RelationSnapshot { return .{ .root = &self.root, .rows = if (self.handle) |*handle| .{ .live = handle } else .{ .materialized = self.rows }, }; } pub fn deinit(self: *RelationView) void { self.root.deinit(); if (self.handle) |*handle| handle.deinit(); version.freeRelationRows(self.allocator, self.rows); self.* = undefined; }};pub const DatabaseView = struct { allocator: Allocator, relations: []RelationView, snapshots: []diff_mod.RelationSnapshot, conflicts: history_mod.ConflictArtifacts, pub fn snapshot(self: *const DatabaseView) merge_mod.DatabaseSnapshot { return .{ .relations = self.snapshots, .conflicts = .{ .root = self.conflicts.root, .artifacts = self.conflicts.artifacts, }, }; } pub fn deinit(self: *DatabaseView) void { for (self.relations) |*relation_view| relation_view.deinit(); if (self.relations.len != 0) self.allocator.free(self.relations); if (self.snapshots.len != 0) self.allocator.free(self.snapshots); self.conflicts.deinit(); self.* = undefined; }};pub const Connection = struct { catalog: catalog_mod.Catalog, session: session_mod.DatabaseSession, recovery_required: bool = false, pub fn init(catalog: catalog_mod.Catalog, session: session_mod.DatabaseSession) Connection { return .{ .catalog = catalog, .session = session, }; } pub fn create(allocator: Allocator, database: *file.Database, history: *history_mod.History, options: Options) Error!Connection { var catalog = try catalog_mod.Catalog.open(database, options.catalog); var value = try version.databaseValue( allocator, &catalog, version.ConflictRoot.empty().hash, ); var value_live = true; errdefer if (value_live) value.deinit(); const root_commit = version.Commit.init(value.root.hash, &.{}); try database.syncWal(); try publishDatabaseValue(history, &value); try history.putCommit(root_commit); _ = try history.createBranch(options.branch, root_commit.hash); const checkout_value = try history.checkoutBranch(options.branch); var root = value.intoRoot(); value_live = false; const session = session_mod.DatabaseSession.initWithRoot(allocator, checkout_value, &root); return Connection.init(catalog, session); } pub fn openLocal(allocator: Allocator, database: *file.Database, options: Options) Error!Connection { const catalog = try catalog_mod.Catalog.open(database, options.catalog); var root = try version.databaseRootMaintained( allocator, &catalog, version.ConflictRoot.empty().hash, ); errdefer root.deinit(); const root_commit = version.Commit.init(root.hash, &.{}); const checkout_value = branch_mod.checkout(.{ .name = options.branch, .target = root_commit.hash, }, root.hash); return Connection.init( catalog, session_mod.DatabaseSession.initWithRoot(allocator, checkout_value, &root), ); } pub fn open(allocator: Allocator, database: *file.Database, history: *history_mod.History, options: Options) Error!Connection { try recoverFastForward(allocator, database, history, options.catalog); const catalog = try catalog_mod.Catalog.open(database, options.catalog); const checkout_value = try history.checkoutBranch(options.branch); var target = try history.databaseRoot(allocator, checkout_value.working.working); defer target.deinit(); try history.validateConflictRoot(target.conflicts); var root = try version.databaseRootMaintained(allocator, &catalog, target.conflicts); errdefer root.deinit(); const live_checkout = checkout_value.withWorking(root.hash); return Connection.init(catalog, session_mod.DatabaseSession.initWithRoot(allocator, live_checkout, &root)); } pub fn adoptRebuiltHistory(self: *Connection, history: *history_mod.History, branch_name: []const u8) Error!void { try self.ensureUsable(); std.debug.assert(history.len() == 0); const allocator = self.session.allocator; var value = try version.databaseValue( allocator, &self.catalog, version.ConflictRoot.empty().hash, ); var value_live = true; errdefer if (value_live) value.deinit(); const root_commit = version.Commit.init(value.root.hash, &.{}); try self.catalog.database.syncWal(); try publishDatabaseValue(history, &value); try history.putCommit(root_commit); _ = try history.createBranch(branch_name, root_commit.hash); const checkout_value = try history.checkoutBranch(branch_name); var root = value.intoRoot(); value_live = false; self.session.reinitWithRoot(checkout_value, &root); } pub fn deinit(self: *Connection) void { self.session.deinit(); self.* = undefined; } pub fn lastRowId(self: *const Connection, allocator: Allocator, table_name: []const u8) Error!?i64 { try self.ensureUsable(); var handle = try self.catalog.openRelation(allocator, table_name); defer handle.deinit(); return try handle.relation.lastRowId(); } pub fn prepare(self: *const Connection, allocator: Allocator, source: []const u8) statement_mod.Error!statement_mod.Prepared { try self.ensureUsable(); return try statement_mod.prepare(&self.catalog, allocator, source); } pub fn execute(self: *Connection, allocator: Allocator, source: []const u8, options: ExecuteOptions) statement_mod.Error!statement_mod.Result { var prepared = try self.prepare(allocator, source); defer prepared.deinit(); return try self.executePrepared(&prepared, allocator, options); } pub fn executePrepared(self: *Connection, prepared: *statement_mod.Prepared, allocator: Allocator, options: ExecuteOptions) statement_mod.Error!statement_mod.Result { try self.ensureUsable(); return try prepared.execute(allocator, options.statement(&self.session)); } /// Opens a cursor over the rows `prepared` selects in `target`. pub fn openCursor( self: *Connection, target: *statement_mod.Cursor, prepared: *statement_mod.Prepared, allocator: Allocator, ) statement_mod.Error!void { try self.ensureUsable(); try prepared.openCursor(target, allocator); } pub fn beginWrite( self: *Connection, workspace: *session_mod.DatabaseWrite.Workspace, flush_allocator: Allocator, options: WriteOptions, ) session_mod.DatabaseError!session_mod.DatabaseWrite { try self.ensureUsable(); return try self.session.beginWrite( workspace, flush_allocator, options.limits, options.commit(), ); } pub fn stage(self: *Connection) Error!void { try self.ensureUsable(); self.session.stage(); } pub fn commit(self: *Connection, history: *history_mod.History) Error!version.Hash { try self.ensureUsable(); if (!self.session.checkout.working.hasStaged()) return error.NoStagedRoot; try self.catalog.database.syncWal(); try self.publishStagedValue(history, self.session.checkout.working.staged); return try self.session.commit(history); } pub fn commitLocal(self: *Connection) Error!version.Hash { try self.ensureUsable(); if (!self.session.checkout.working.hasStaged()) return error.NoStagedRoot; const root = self.session.checkout.working.staged; const parents = [_]version.Hash{self.session.checkout.head}; const commit_value = version.Commit.init(root, &parents); try self.session.advance(commit_value.hash, root); return commit_value.hash; } pub fn mergeCommit(self: *Connection, history: *history_mod.History, theirs: version.Hash) Error!version.Hash { try self.ensureUsable(); const root = self.session.checkout.working.working; try self.catalog.database.syncWal(); try self.publishStagedValue(history, root); const commit_hash = try history.mergeCommitBranch(self.session.checkout.name, root, theirs); try self.session.advance(commit_hash, root); return commit_hash; } pub fn createBranch(self: *const Connection, history: *history_mod.History, name: []const u8) history_mod.Error!version.Ref { try self.ensureUsable(); return try history.createBranch(name, self.session.checkout.head); } pub fn checkoutBranch(self: *Connection, allocator: Allocator, history: *const history_mod.History, name: []const u8) Error!void { try self.ensureUsable(); const checkout_value = try history.checkoutBranch(name); var value = try history.databaseValue(allocator, checkout_value.working.working); var value_live = true; errdefer if (value_live) value.deinit(); try history.validateConflictRoot(value.root.conflicts); try self.materializeDatabaseValue(allocator, &value, .{ .durability = .synced }); var root = value.intoRoot(); value_live = false; self.session.reinitWithRoot(checkout_value, &root); } pub fn fastForwardBranch(self: *Connection, allocator: Allocator, history: *history_mod.History, target: version.Hash) Error!void { try self.ensureUsable(); var next_ref = (try history.ref(self.session.checkout.name)) orelse return error.RefNotFound; const baseline = try self.validateFastForwardBaseline(allocator, history, next_ref); const entries = try history.commitEntries(allocator); defer allocator.free(entries); try branch_mod.fastForwardRef(allocator, entries, &next_ref, target); const commit_value = try history.commitValue(target); var value = try history.databaseValue(allocator, commit_value.root); var value_live = true; errdefer if (value_live) value.deinit(); try history.validateConflictRoot(value.root.conflicts); var plan = try DatabaseMaterialization.init(self, allocator, &value); defer plan.deinit(); try self.coordinateFastForward( allocator, history, next_ref, baseline, &value, &value_live, &plan, ); } fn validateFastForwardBaseline( self: *Connection, allocator: Allocator, history: *const history_mod.History, current_ref: version.Ref, ) Error!version.Hash { if (!self.catalog.database.walSynced()) return error.WorkingSetChanged; if (!version.same(current_ref.target, self.session.checkout.head)) return error.RefChanged; const working = self.session.checkout.working; if (!version.same(working.base, working.working) or !version.same(working.base, working.staged) or !version.same(working.base, self.session.workingRoot().hash) or self.session.pendingRelations() != 0) { return error.WorkingSetChanged; } const current_commit = try history.commitValue(self.session.checkout.head); if (!version.same(current_commit.root, working.base)) return error.WorkingSetChanged; var live_root = try version.databaseRootMaintained( allocator, &self.catalog, self.session.workingRoot().conflicts, ); defer live_root.deinit(); if (!version.same(live_root.hash, working.base)) return error.WorkingSetChanged; return working.base; } fn coordinateFastForward( self: *Connection, allocator: Allocator, history: *history_mod.History, next_ref: version.Ref, baseline: version.Hash, value: *version.DatabaseValue, value_live: *bool, plan: *DatabaseMaterialization, ) Error!void { try self.catalog.database.syncWal(); const savepoint = try self.catalog.database.savepoint(); try self.catalog.database.beginCoordinator(); var coordinator_active = true; errdefer if (coordinator_active and !self.catalog.database.requiresRecovery()) { self.catalog.database.endCoordinator(); }; var update = history.beginFastForward( next_ref.name, self.session.checkout.head, next_ref.target, ) catch |err| { if (err == error.RecoveryRequired) { self.poisonFastForward(history); return error.RecoveryRequired; } self.catalog.database.endCoordinator(); coordinator_active = false; return err; }; plan.apply(self, .{ .durability = .synced }) catch |err| { try self.restoreFastForward(history, &update, savepoint, baseline); coordinator_active = false; return err; }; self.verifyMaterializedDatabaseRoot(allocator, &value.root) catch |err| { try self.restoreFastForward(history, &update, savepoint, baseline); coordinator_active = false; return err; }; update.commit() catch |err| { if (err == error.RecoveryRequired) { self.poisonFastForward(history); return error.RecoveryRequired; } try self.restoreFastForward(history, &update, savepoint, baseline); coordinator_active = false; return err; }; const checkout_value = branch_mod.checkout(next_ref, value.root.hash); var root = value.intoRoot(); value_live.* = false; self.session.reinitWithRoot(checkout_value, &root); try self.completeFastForward(history, &update); self.catalog.database.endCoordinator(); coordinator_active = false; } fn verifyMaterializedDatabaseRoot( self: *Connection, allocator: Allocator, target: *const version.DatabaseRoot, ) Error!void { var names = try self.catalog.relationNames(allocator); defer names.deinit(); if (names.names.len != target.entries.len) return error.InvalidHistory; for (names.names) |name| { const entry = relationEntry(target.entries, name) orelse return error.InvalidHistory; var state = try self.catalog.readRelation(allocator, name); defer state.deinit(); const live = try version.relationKey(name, &state.handle, state.relationStats()); if (!version.same(live.hash, entry.hash)) return error.InvalidHistory; } var verified = try version.databaseRootMaintained( allocator, &self.catalog, target.conflicts, ); defer verified.deinit(); if (!version.same(verified.hash, target.hash)) return error.InvalidHistory; } fn restoreFastForward( self: *Connection, history: *history_mod.History, update: *history_mod.FastForwardUpdate, savepoint: file.Savepoint, baseline: version.Hash, ) Error!void { self.rollbackFastForward(update, savepoint, baseline) catch { self.poisonFastForward(history); return error.RecoveryRequired; }; } fn completeFastForward( self: *Connection, history: *history_mod.History, update: *history_mod.FastForwardUpdate, ) Error!void { update.complete() catch |err| { if (err != error.RecoveryRequired) update.complete() catch { self.poisonFastForward(history); return error.RecoveryRequired; } else { self.poisonFastForward(history); return error.RecoveryRequired; } }; } fn rollbackFastForward( self: *Connection, update: *history_mod.FastForwardUpdate, savepoint: file.Savepoint, baseline: version.Hash, ) Error!void { try self.catalog.database.restore(savepoint); try self.catalog.database.syncWal(); var restored = try version.databaseRootMaintained( self.session.allocator, &self.catalog, self.session.workingRoot().conflicts, ); defer restored.deinit(); if (!version.same(restored.hash, baseline)) return error.RecoveryRequired; try update.abort(); try update.complete(); self.catalog.database.endCoordinator(); } fn poisonFastForward(self: *Connection, history: *history_mod.History) void { self.recovery_required = true; self.catalog.database.poison(); history.poison(); } fn ensureUsable(self: *const Connection) error{RecoveryRequired}!void { if (self.recovery_required or self.catalog.database.requiresRecovery()) { return error.RecoveryRequired; } } pub fn mergeBase(self: *const Connection, allocator: Allocator, history: *const history_mod.History, theirs: []const u8) history_mod.Error!?version.Hash { try self.ensureUsable(); const theirs_ref = (try history.ref(theirs)) orelse return error.RefNotFound; const entries = try history.commitEntries(allocator); defer allocator.free(entries); return try branch_mod.mergeBase(allocator, entries, self.session.checkout.head, theirs_ref.target); } pub fn relationView(self: *const Connection, allocator: Allocator, name: []const u8) Error!RelationView { try self.ensureUsable(); var state = try self.catalog.readRelation(allocator, name); errdefer state.handle.deinit(); defer if (state.stats) |*relation_stats| relation_stats.deinit(); var root = try version.relationRoot( allocator, name, state.schema, &state.handle, state.relationStats(), ); errdefer root.deinit(); return .{ .allocator = allocator, .root = root, .handle = state.handle, }; } pub fn databaseView(self: *const Connection, allocator: Allocator, history: *const history_mod.History, names: []const []const u8) Error!DatabaseView { try self.ensureUsable(); var conflicts = try history.conflictArtifacts(allocator, self.session.workingRoot().conflicts); errdefer conflicts.deinit(); const relations = try allocator.alloc(RelationView, names.len); errdefer allocator.free(relations); var relation_count: usize = 0; errdefer for (relations[0..relation_count]) |*relation_view| relation_view.deinit(); const snapshots = try allocator.alloc(diff_mod.RelationSnapshot, names.len); errdefer allocator.free(snapshots); for (names, relations, snapshots) |name, *relation_view, *snapshot| { relation_view.* = try self.relationView(allocator, name); relation_count += 1; snapshot.* = relation_view.snapshot(); } return .{ .allocator = allocator, .relations = relations, .snapshots = snapshots, .conflicts = conflicts, }; } pub fn databaseViewAtCommit(self: *const Connection, allocator: Allocator, history: *const history_mod.History, commit_hash: version.Hash) Error!DatabaseView { try self.ensureUsable(); var root = try history.commitDatabaseRoot(allocator, commit_hash); defer root.deinit(); return try databaseViewFromRoot(allocator, history, root); } pub fn diffRelation(self: *const Connection, allocator: Allocator, name: []const u8, other: diff_mod.RelationSnapshot) Error!diff_mod.RelationDiff { try self.ensureUsable(); var local = try self.relationView(allocator, name); defer local.deinit(); return try diff_mod.relation(allocator, local.snapshot(), other); } pub fn mergeDatabase(self: *Connection, allocator: Allocator, history: *history_mod.History, base: merge_mod.DatabaseSnapshot, theirs: merge_mod.DatabaseSnapshot, options: MergeOptions) Error!merge_mod.DatabaseMerge { try self.ensureUsable(); var ours = try self.materializedWorkingValue(allocator); defer ours.deinit(); var ours_conflicts = try history.conflictArtifacts(allocator, ours.root.conflicts); defer ours_conflicts.deinit(); var result = try merge_mod.mergeDatabase(allocator, base, .{ .value = &ours, .conflicts = .{ .root = ours_conflicts.root, .artifacts = ours_conflicts.artifacts, }, }, theirs); errdefer result.deinit(); try self.materializeDatabaseMerge(allocator, history, &result, options.commit); var root = try result.value.root.clone(self.session.allocator); self.session.applyRoot(&root); return result; } pub fn mergeBranch(self: *Connection, allocator: Allocator, history: *history_mod.History, theirs: []const u8, options: MergeOptions) Error!merge_mod.DatabaseMerge { try self.ensureUsable(); const theirs_ref = (try history.ref(theirs)) orelse return error.RefNotFound; const entries = try history.commitEntries(allocator); defer allocator.free(entries); const base_commit = (try branch_mod.mergeBase(allocator, entries, self.session.checkout.head, theirs_ref.target)) orelse return error.NoMergeBase; var base_view = try self.databaseViewAtCommit(allocator, history, base_commit); defer base_view.deinit(); var theirs_view = try self.databaseViewAtCommit(allocator, history, theirs_ref.target); defer theirs_view.deinit(); return try self.mergeDatabase(allocator, history, base_view.snapshot(), theirs_view.snapshot(), options); } fn replaceConflictRoot(self: *Connection, conflicts: version.ConflictRoot) Error!version.Hash { var root = try version.DatabaseRoot.initSorted(self.session.allocator, self.session.workingRoot().entries, conflicts); self.session.applyRoot(&root); return (try self.workingRoot()); } pub fn conflictArtifacts(self: *const Connection, allocator: Allocator, history: *const history_mod.History) Error!history_mod.ConflictArtifacts { try self.ensureUsable(); return try history.conflictArtifacts(allocator, self.session.workingRoot().conflicts); } pub fn resolveConflicts(self: *Connection, allocator: Allocator, history: *history_mod.History, resolved: []const version.Hash) Error!version.Hash { try self.ensureUsable(); if (resolved.len == 0) return try self.workingRoot(); var entries = try history.conflictEntries(allocator, self.session.workingRoot().conflicts); defer entries.deinit(); for (resolved) |hash| { if (!hasConflictHash(entries.entries, hash)) return error.ConflictNotFound; } var remaining: std.ArrayList(version.ConflictEntry) = .empty; defer remaining.deinit(allocator); for (entries.entries) |entry| { if (!hasConflictHashValue(resolved, entry.hash)) try remaining.append(allocator, entry); } const root = try history.putConflictRoot(remaining.items); return try self.replaceConflictRoot(root); } pub fn checkout(self: *const Connection) Error!@import("branch.zig").Checkout { try self.ensureUsable(); return self.session.checkout; } pub fn workingRoot(self: *const Connection) Error!version.Hash { try self.ensureUsable(); return self.session.checkout.working.working; } fn relationViewFromRoot(allocator: Allocator, history: *const history_mod.History, root_hash: version.Hash) Error!RelationView { var root = try history.relationRoot(allocator, root_hash); errdefer root.deinit(); const rows = try history.relationRows(allocator, root_hash); errdefer version.freeRelationRows(allocator, rows); return .{ .allocator = allocator, .root = root, .rows = rows, }; } fn databaseViewFromRoot(allocator: Allocator, history: *const history_mod.History, root: version.DatabaseRoot) Error!DatabaseView { var conflicts = try history.conflictArtifacts(allocator, root.conflicts); errdefer conflicts.deinit(); const relations = try allocator.alloc(RelationView, root.entries.len); errdefer allocator.free(relations); var relation_count: usize = 0; errdefer for (relations[0..relation_count]) |*relation_view| relation_view.deinit(); const snapshots = try allocator.alloc(diff_mod.RelationSnapshot, root.entries.len); errdefer allocator.free(snapshots); for (root.entries, relations, snapshots) |entry, *relation_view, *snapshot| { relation_view.* = try relationViewFromRoot(allocator, history, entry.hash); relation_count += 1; snapshot.* = relation_view.snapshot(); } return .{ .allocator = allocator, .relations = relations, .snapshots = snapshots, .conflicts = conflicts, }; } fn materializeDatabaseValue(self: *Connection, allocator: Allocator, value: *const version.DatabaseValue, options: file.CommitOptions) Error!void { var plan = try DatabaseMaterialization.init(self, allocator, value); defer plan.deinit(); try plan.apply(self, options); } fn materializeDatabaseMerge( self: *Connection, allocator: Allocator, history: *history_mod.History, result: *const merge_mod.DatabaseMerge, options: file.CommitOptions, ) Error!void { if (!version.same(result.value.root.conflicts, result.conflict_root.hash)) { return error.InvalidHistory; } try result.persistConflicts(history); try history.validateConflictRoot(result.value.root.conflicts); try self.materializeDatabaseValue(allocator, &result.value, options); try self.verifyMaterializedDatabaseRoot(allocator, &result.value.root); } fn publishStagedValue(self: *Connection, history: *history_mod.History, root: version.Hash) Error!void { const allocator = self.session.allocator; const working = self.session.workingRoot(); if (!version.same(working.hash, root)) return error.InvalidHistory; try history.validateConflictRoot(working.conflicts); var live_root = try version.databaseRootMaintained( allocator, &self.catalog, working.conflicts, ); defer live_root.deinit(); if (!version.same(live_root.hash, working.hash)) return error.InvalidHistory; var batch = try history.beginWriteBatch(); errdefer batch.deinit(); var base_root: ?version.DatabaseRoot = null; defer if (base_root) |*owned| owned.deinit(); base_root = history.commitDatabaseRoot(allocator, self.session.checkout.head) catch null; for (working.entries) |entry| { const base = self.publicationBase(&base_root, entry.name); const need = try history.relationRowsNeed(allocator, entry.hash, base); var state = try self.catalog.readRelation(allocator, entry.name); defer state.deinit(); const live_key = try version.relationKey( entry.name, &state.handle, state.relationStats(), ); if (!version.same(live_key.hash, entry.hash)) return error.InvalidHistory; if (need == .none and history.hasRelationRoot(entry.hash)) continue; var relation_root = try version.relationRootMaintained( allocator, entry.name, state.schema, &state.handle, state.relationStats(), ); defer relation_root.deinit(); try history.putRelationRoot(relation_root); switch (need) { .none => {}, .full => { var writer = try history.beginRelationRows(entry.hash); defer writer.deinit(); try streamRelationRows(&writer, allocator, &state.handle, null); try writer.finish(); }, .suffix => |plan| { var writer = try history.beginRelationRowsSuffix(entry.hash, base.?, plan); defer writer.deinit(); try streamRelationRows(&writer, allocator, &state.handle, plan.boundary); try writer.finish(); }, } } try history.putDatabaseRoot(working.*); try batch.finish(); } fn streamRelationRows( writer: *history_mod.RelationRowsWriter, allocator: Allocator, handle: *const catalog_mod.RelationHandle, boundary: ?i64, ) Error!void { if (boundary) |value| { if (value == std.math.maxInt(i64)) return; } const start: ?i64 = if (boundary) |value| value + 1 else null; var scan: relation_mod.Scan = undefined; try handle.relation.scan(&scan, allocator, start, null); defer scan.deinit(); while (try scan.next()) |row_entry| try writer.append(row_entry.rowid, row_entry.bytes); } fn publicationBase(self: *const Connection, base_root: *const ?version.DatabaseRoot, name: []const u8) ?history_mod.IncrementalBase { const root = base_root.* orelse return null; const edited = self.session.pendingEditRowids(root.hash, name) orelse return null; for (root.entries) |entry| { if (std.mem.eql(u8, entry.name, name)) return .{ .root = entry.hash, .edited = edited }; } return null; } pub fn materializedWorkingValue(self: *Connection, allocator: Allocator) Error!version.DatabaseValue { try self.ensureUsable(); const working = self.session.workingRoot(); return try version.databaseValue(allocator, &self.catalog, working.conflicts); }};fn recoverFastForward( allocator: Allocator, database: *file.Database, history: *history_mod.History, catalog_options: catalog_mod.Options,) Error!void { const active = history.fastForwardRecovery() orelse return; const id = active.id; const decision = active.decision; const selected_head = switch (decision) { .pending, .baseline => active.expected, .target => active.target, }; const name = active.name; try database.beginCoordinator(); var coordinator_active = true; var recovered = false; defer { if (coordinator_active and !database.requiresRecovery()) database.endCoordinator(); if (!recovered) { database.poison(); history.poison(); } } const commit_value = try history.commitValue(selected_head); var value = try history.databaseValue(allocator, commit_value.root); defer value.deinit(); try history.validateConflictRoot(value.root.conflicts); const catalog = try catalog_mod.Catalog.open(database, catalog_options); var live_root = try version.databaseRootMaintained( allocator, &catalog, value.root.conflicts, ); var live_root_owned = true; defer if (live_root_owned) live_root.deinit(); const checkout_value = branch_mod.checkout(.{ .name = name, .target = selected_head, }, live_root.hash); var connection = Connection.init( catalog, session_mod.DatabaseSession.initWithRoot(allocator, checkout_value, &live_root), ); live_root_owned = false; defer connection.deinit(); if (!version.same(connection.session.workingRoot().hash, value.root.hash)) { var plan = try DatabaseMaterialization.init(&connection, allocator, &value); defer plan.deinit(); try plan.apply(&connection, .{ .durability = .synced }); } else { try database.syncWal(); } try connection.verifyMaterializedDatabaseRoot(allocator, &value.root); var update = history_mod.FastForwardUpdate{ .history = history, .id = id }; if (decision == .pending) try update.abort(); try update.complete(); database.endCoordinator(); coordinator_active = false; recovered = true;}const PlannedRelation = struct { target: *const version.RelationValue, definition: catalog_mod.RelationDefinition, live: ?RelationView = null, edits: []relation_mod.Edit = &.{}, prepared_stats: ?catalog_mod.PreparedRelationStats = null, created: ?catalog_mod.MaterializedRelation = null, clear_live_stats: bool = false, fn deinit(self: *PlannedRelation, allocator: Allocator) void { if (self.prepared_stats) |*prepared| prepared.deinit(); if (self.edits.len != 0) allocator.free(self.edits); if (self.live) |*live| live.deinit(); self.* = undefined; }};const DatabaseMaterialization = struct { allocator: Allocator, names: catalog_mod.RelationNames, relations: []PlannedRelation, relation_count: usize = 0, fn init( connection: *Connection, allocator: Allocator, value: *const version.DatabaseValue, ) Error!DatabaseMaterialization { try validateValue(value); var names = try connection.catalog.relationNames(allocator); errdefer names.deinit(); const relations = try allocator.alloc(PlannedRelation, value.relations.len); var relation_count: usize = 0; errdefer { for (relations[0..relation_count]) |*relation| relation.deinit(allocator); if (relations.len != 0) allocator.free(relations); } for (value.relations, relations, 0..) |*target, *planned, relation_offset| { try validateTarget(value, target, relation_offset); planned.* = try planRelation( connection, allocator, target, names.names, ); relation_count += 1; } return .{ .allocator = allocator, .names = names, .relations = relations, .relation_count = relation_count, }; } fn validateValue(value: *const version.DatabaseValue) Error!void { if (value.root.format != version.format_version or value.root.feature != 0) { return error.InvalidHistory; } if (value.root.entries.len != value.relations.len) return error.InvalidHistory; for (value.root.entries, 0..) |entry, index| { if (entry.name.len == 0) return error.InvalidHistory; if (index != 0 and simd.order(Bytes, value.root.entries[index - 1].name, entry.name) != .lt) { return error.InvalidHistory; } } const rebuilt = version.DatabaseRoot.init( value.root.entries, .{ .hash = value.root.conflicts }, ); if (!version.same(rebuilt.hash, value.root.hash)) return error.InvalidHistory; } fn validateTarget( value: *const version.DatabaseValue, target: *const version.RelationValue, relation_offset: usize, ) Error!void { for (value.relations[0..relation_offset]) |previous| { if (std.mem.eql(u8, previous.root.name, target.root.name)) { return error.InvalidHistory; } } const entry = relationEntry( value.root.entries, target.root.name, ) orelse return error.InvalidHistory; if (!version.same(entry.hash, target.root.hash)) return error.InvalidHistory; if (!relationRowsStrictlySorted(target.rows)) return error.InvalidHistory; } fn planRelation( connection: *Connection, allocator: Allocator, target: *const version.RelationValue, names: []const []const u8, ) Error!PlannedRelation { var planned = PlannedRelation{ .target = target, .definition = .{ .name = target.root.name, .columns = target.root.schema_descriptor.columns, .indexes = target.root.schema_descriptor.indexes, }, }; errdefer planned.deinit(allocator); if (target.root.catalog.format != catalog_mod.format_version) { return error.InvalidHistory; } catalog_mod.validateDefinition(planned.definition) catch |err| switch (err) { error.OutOfMemory => return err, else => return error.InvalidHistory, }; planned.prepared_stats = try validateRelationIdentity( connection, allocator, target, planned.definition, ); if (relationNameExists(names, target.root.name)) { planned.live = try connection.relationView(allocator, target.root.name); const live = &planned.live.?; if (!version.same(live.root.schema, target.root.schema)) { return error.UnsupportedCheckoutRoot; } const stats_mismatch = !version.same( live.root.stats.hash, target.root.stats.hash, ); if (stats_mismatch and relationRootHasStats(&target.root)) { return error.UnsupportedCheckoutRoot; } planned.clear_live_stats = stats_mismatch; planned.edits = try relationMaterializationEdits( allocator, live.snapshot(), target, ); } else { planned.edits = try relationPutEdits(allocator, target.rows); } return planned; } fn validateRelationIdentity( connection: *Connection, allocator: Allocator, target: *const version.RelationValue, definition: catalog_mod.RelationDefinition, ) Error!?catalog_mod.PreparedRelationStats { if (!relationRootHasStats(&target.root)) { var rebuilt = version.relationRootFromRows( allocator, &target.root, target.rows, ) catch |err| switch (err) { error.OutOfMemory => return err, else => return error.InvalidHistory, }; defer rebuilt.deinit(); if (!version.same(rebuilt.hash, target.root.hash)) return error.InvalidHistory; return null; } const table_summary = target.root.stats.table orelse return error.InvalidHistory; if (target.root.stats.indexes != target.root.indexes.len) { return error.InvalidHistory; } var index_summaries: [relation_mod.max_indexes]tree.Summary = undefined; if (target.root.indexes.len > index_summaries.len) return error.InvalidHistory; for ( target.root.indexes, index_summaries[0..target.root.indexes.len], ) |index_root, *summary| { summary.* = index_root.map.summary; } const puts = try relationPuts(allocator, target.rows); defer if (puts.len != 0) allocator.free(puts); var prepared = connection.catalog.prepareRelationStats( allocator, definition, puts, table_summary, index_summaries[0..target.root.indexes.len], ) catch |err| switch (err) { error.OutOfMemory => return err, else => return error.InvalidHistory, }; errdefer prepared.deinit(); var rebuilt = version.relationRootFromRowsWithStats( allocator, &target.root, target.rows, &prepared.stats, ) catch |err| switch (err) { error.OutOfMemory => return err, else => return error.InvalidHistory, }; defer rebuilt.deinit(); if (!version.same(rebuilt.hash, target.root.hash)) return error.InvalidHistory; return prepared; } fn deinit(self: *DatabaseMaterialization) void { for (self.relations[0..self.relation_count]) |*relation| relation.deinit(self.allocator); if (self.relations.len != 0) self.allocator.free(self.relations); self.names.deinit(); self.* = undefined; } fn apply( self: *DatabaseMaterialization, connection: *Connection, options: file.CommitOptions, ) Error!void { var materialization = try connection.catalog.beginMaterialization(self.allocator); defer materialization.deinit(); for (self.names.names) |name| { if (!plannedRelationExists(self.relations[0..self.relation_count], name)) { try materialization.dropRelation(name); } } for (self.relations[0..self.relation_count]) |*planned| { if (planned.live != null) continue; planned.created = try materialization.createRelation( planned.definition, if (planned.prepared_stats) |*prepared| prepared else null, ); } for (self.relations[0..self.relation_count]) |*planned| { if (planned.clear_live_stats) { try materialization.clearRelationStats(planned.target.root.name); } if (planned.edits.len != 0) { if (planned.live) |*live| { try live.handle.?.relation.applyEditsIn( self.allocator, materialization.treeWrite(), planned.edits, ); } else { try planned.created.?.relation.applyEditsIn( self.allocator, materialization.treeWrite(), planned.edits, ); } } if (planned.created) |*created| { if (planned.prepared_stats) |*prepared| { try materialization.refreshRelationStats(planned.definition, created, prepared); } } } _ = try materialization.commit(options); }};fn relationNameExists(names: []const []const u8, target: []const u8) bool { for (names) |name| { if (std.mem.eql(u8, name, target)) return true; } return false;}fn relationEntry(entries: []const version.RelationEntry, name: []const u8) ?version.RelationEntry { for (entries) |entry| { if (std.mem.eql(u8, entry.name, name)) return entry; } return null;}fn relationRootHasStats(root: *const version.RelationRoot) bool { return !version.same(root.stats.hash, version.emptyHash("sql.stats.none"));}fn plannedRelationExists(relations: []const PlannedRelation, name: []const u8) bool { for (relations) |relation| { if (std.mem.eql(u8, relation.target.root.name, name)) return true; } return false;}fn relationRowsStrictlySorted(rows: []const version.RelationRow) bool { var previous: ?i64 = null; for (rows) |row_value| { if (previous) |rowid| { if (row_value.rowid <= rowid) return false; } previous = row_value.rowid; } return true;}fn relationPuts( allocator: Allocator, rows: []const version.RelationRow,) Allocator.Error![]relation_mod.Edit.Put { const puts = try allocator.alloc(relation_mod.Edit.Put, rows.len); for (rows, puts) |row_value, *put| { put.* = .{ .rowid = row_value.rowid, .bytes = row_value.bytes, }; } return puts;}fn relationPutEdits( allocator: Allocator, rows: []const version.RelationRow,) Allocator.Error![]relation_mod.Edit { const edits = try allocator.alloc(relation_mod.Edit, rows.len); for (rows, edits) |row_value, *edit| { edit.* = .{ .put = .{ .rowid = row_value.rowid, .bytes = row_value.bytes, } }; } return edits;}fn relationMaterializationEdits( allocator: Allocator, live: diff_mod.RelationSnapshot, target: *const version.RelationValue,) Error![]relation_mod.Edit { var result = try diff_mod.relation(allocator, live, .{ .root = &target.root, .rows = .{ .materialized = target.rows }, }); defer result.deinit(); if (result.schema_changed) return error.UnsupportedCheckoutRoot; var edits: std.ArrayList(relation_mod.Edit) = .empty; errdefer edits.deinit(allocator); for (result.changes) |change| { switch (change.kind) { .added, .modified => try edits.append(allocator, .{ .put = .{ .rowid = change.rowid, .bytes = relationRowBytes(target.rows, change.rowid) orelse return error.InvalidHistory, } }), .removed => try edits.append(allocator, .{ .delete = change.rowid }), .schema => return error.UnsupportedCheckoutRoot, } } return try edits.toOwnedSlice(allocator);}fn relationRowBytes(rows: []const version.RelationRow, rowid: i64) ?[]const u8 { for (rows) |row_value| { if (row_value.rowid == rowid) return row_value.bytes; if (row_value.rowid > rowid) return null; } return null;}fn publishDatabaseValue(history: *history_mod.History, value: *const version.DatabaseValue) history_mod.Error!void { try history.putDatabaseValue(value);}test "connection commits reuse history chunks for append shaped edits" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "chunks.db", .wal = "chunks.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 2048 }); var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "chunks.history", .recovery = .reject }); defer history.deinit(); var connection = try Connection.create(std.testing.allocator, &database, &history, .{}); defer connection.deinit(); var created = try connection.execute(std.testing.allocator, "CREATE TABLE items (name)", .{ .durability = .buffered }); created.deinit(std.testing.allocator); try connection.stage(); _ = try connection.commit(&history); var rowid: i64 = 1; var statement_buffer: [128]u8 = undefined; while (rowid <= 600) : (rowid += 1) { const source = try std.fmt.bufPrint(&statement_buffer, "INSERT INTO items VALUES ({d}, 'row-{d}')", .{ rowid, rowid }); var inserted = try connection.execute(std.testing.allocator, source, .{ .durability = .buffered }); inserted.deinit(std.testing.allocator); } try connection.stage(); _ = try connection.commit(&history); const seeded_chunks = history.row_chunks.items.len; try std.testing.expect(seeded_chunks >= 4); while (rowid <= 603) : (rowid += 1) { const source = try std.fmt.bufPrint(&statement_buffer, "INSERT INTO items VALUES ({d}, 'row-{d}')", .{ rowid, rowid }); var inserted = try connection.execute(std.testing.allocator, source, .{ .durability = .buffered }); inserted.deinit(std.testing.allocator); } try connection.stage(); try expectAppendPrefixReuse(&connection, &history); const append_commit = try connection.commit(&history); const append_chunks = history.row_chunks.items.len; try std.testing.expect(append_chunks - seeded_chunks <= 2); var update = try connection.execute(std.testing.allocator, "UPDATE items SET name = 'renamed' WHERE rowid = 5", .{ .durability = .buffered }); update.deinit(std.testing.allocator); try connection.stage(); const update_commit = try connection.commit(&history); const update_chunks = history.row_chunks.items.len; try std.testing.expect(update_chunks - append_chunks <= 3); var remove = try connection.execute(std.testing.allocator, "DELETE FROM items WHERE rowid = 300", .{ .durability = .buffered }); remove.deinit(std.testing.allocator); try connection.stage(); const delete_commit = try connection.commit(&history); for ([_]version.Hash{ append_commit, update_commit, delete_commit }) |commit_hash| { const commit_value = try history.commitValue(commit_hash); var value = try history.databaseValue(std.testing.allocator, commit_value.root); value.deinit(); } var final = try history.databaseValue(std.testing.allocator, (try connection.checkout()).working.working); defer final.deinit(); const relation = final.findRelation("items") orelse return error.TestUnexpectedResult; try std.testing.expectEqual(@as(usize, 602), relation.rows.len);}fn expectAppendPrefixReuse(connection: *Connection, history: *history_mod.History) !void { const allocator = std.testing.allocator; var base_root: ?version.DatabaseRoot = try history.commitDatabaseRoot( allocator, connection.session.checkout.head, ); defer base_root.?.deinit(); const base = connection.publicationBase(&base_root, "items").?; const working = connection.session.workingRoot(); const entry = relationEntry(working.entries, "items").?; const need = try history.relationRowsNeed(allocator, entry.hash, base); try std.testing.expect(need == .suffix); try std.testing.expect(need.suffix.reused > 0);}test "local connection commits the maintained database without history" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); { var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "local.db", .wal = "local.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 512 }); var connection = try Connection.openLocal(std.testing.allocator, &database, .{}); defer connection.deinit(); try std.testing.expect(!(try connection.checkout()).working.dirty()); var created = try connection.execute( std.testing.allocator, "CREATE TABLE items (name)", .{ .durability = .buffered }, ); created.deinit(std.testing.allocator); try connection.stage(); _ = try connection.commitLocal(); try std.testing.expect(!(try connection.checkout()).working.dirty()); var inserted = try connection.execute( std.testing.allocator, "INSERT INTO items VALUES (1, 'local')", .{ .durability = .buffered }, ); inserted.deinit(std.testing.allocator); try connection.stage(); _ = try connection.commitLocal(); try database.syncWal(); } var reopened_database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "local.db", .wal = "local.wal" }, .header = testingHeader(), }); defer reopened_database.deinit(); var reopened = try Connection.openLocal(std.testing.allocator, &reopened_database, .{}); defer reopened.deinit(); var selected = try reopened.execute( std.testing.allocator, "SELECT name FROM items WHERE rowid = 1", .{ .durability = .buffered }, ); defer selected.deinit(std.testing.allocator); const view = try row.View.init(selected.nextRow().?); try std.testing.expectEqualStrings("local", (try view.column(0)).text); try std.testing.expect(selected.nextRow() == null);}test "connection executes statement writes through its database session" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "connection.db", .wal = "connection.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 640 }); var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "connection.history", .recovery = .reject }); defer history.deinit(); var connection = try Connection.create(std.testing.allocator, &database, &history, .{}); defer connection.deinit(); try std.testing.expect(!(try connection.checkout()).working.dirty()); var created = try connection.execute(std.testing.allocator, "CREATE TABLE items (name DEFAULT 'missing', score, INDEX items_score (score))", .{ .durability = .buffered }); defer created.deinit(std.testing.allocator); const create_flush = switch (created) { .catalog => |flush| flush, else => return error.UnsupportedStatement, }; try std.testing.expect(version.same(create_flush.database, (try connection.workingRoot()))); try std.testing.expect((try connection.checkout()).working.dirty()); var insert = try connection.prepare(std.testing.allocator, "INSERT INTO items (rowid, name, score) VALUES (?1, ?2, ?3)"); defer insert.deinit(); try insert.bind(1, .{ .integer = 4 }); try insert.bind(2, .{ .text = "Ada" }); try insert.bind(3, .{ .integer = 7 }); var inserted = try connection.executePrepared(&insert, std.testing.allocator, .{ .durability = .buffered }); defer inserted.deinit(std.testing.allocator); try std.testing.expect(version.same(inserted.mutation.database, (try connection.workingRoot()))); var selected = try connection.execute(std.testing.allocator, "SELECT name, score, rowid FROM items WHERE score = 7", .{ .durability = .buffered }); defer selected.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), selected.rowCount()); const bytes = selected.nextRow().?; const view = try row.View.init(bytes); try std.testing.expectEqualStrings("Ada", (try view.column(0)).text); try std.testing.expectEqual(@as(i64, 7), (try view.column(1)).integer); try std.testing.expectEqual(@as(i64, 4), (try view.column(2)).integer); try connection.stage(); const commit_hash = try connection.commit(&history); try std.testing.expect(version.same(commit_hash, (try history.ref("main")).?.target)); try std.testing.expect(!(try connection.checkout()).working.dirty()); var committed_root = try history.commitDatabaseRoot(std.testing.allocator, commit_hash); defer committed_root.deinit(); try std.testing.expect(version.same(committed_root.hash, (try connection.workingRoot()))); try std.testing.expect(committed_root.entries.len > 0); for (committed_root.entries) |entry| { var relation_root = try history.relationRoot(std.testing.allocator, entry.hash); defer relation_root.deinit(); try std.testing.expect(version.same(entry.hash, relation_root.hash)); try std.testing.expectEqualStrings(entry.name, relation_root.name); const relation_rows = try history.relationRows(std.testing.allocator, entry.hash); defer version.freeRelationRows(std.testing.allocator, relation_rows); try std.testing.expectEqual(@as(usize, 1), relation_rows.len); try std.testing.expectEqual(@as(i64, 4), relation_rows[0].rowid); } var reopened = try Connection.open(std.testing.allocator, &database, &history, .{}); defer reopened.deinit(); try std.testing.expect(version.same(commit_hash, (try reopened.checkout()).head)); try std.testing.expect(version.same((try connection.workingRoot()), (try reopened.workingRoot()))); try std.testing.expect(version.same((try reopened.workingRoot()), reopened.session.workingRoot().hash));}test "connection publishes catalog session values on commit" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "connection-catalog-commit.db", .wal = "connection-catalog-commit.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 512 }); var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "connection-catalog-commit.history", .recovery = .reject }); defer history.deinit(); var connection = try Connection.create(std.testing.allocator, &database, &history, .{}); defer connection.deinit(); var created = try connection.execute(std.testing.allocator, "CREATE TABLE items (name DEFAULT 'missing', score, INDEX items_score (score))", .{ .durability = .buffered }); defer created.deinit(std.testing.allocator); const create_flush = switch (created) { .catalog => |flush| flush, else => return error.UnsupportedStatement, }; const working_root = connection.session.workingRoot(); try std.testing.expect(version.same(create_flush.database, working_root.hash)); try std.testing.expectEqual(@as(usize, 1), working_root.entries.len); try std.testing.expectError(error.DatabaseRootNotFound, history.databaseRoot(std.testing.allocator, create_flush.database)); try connection.stage(); const commit_hash = try connection.commit(&history); try std.testing.expect(version.same(create_flush.database, connection.session.workingRoot().hash)); var committed = try history.commitDatabaseRoot(std.testing.allocator, commit_hash); defer committed.deinit(); try std.testing.expect(version.same(create_flush.database, committed.hash)); try std.testing.expectEqual(@as(usize, 1), committed.entries.len); var relation_root = try history.relationRoot(std.testing.allocator, committed.entries[0].hash); defer relation_root.deinit(); try std.testing.expectEqualStrings("items", relation_root.name); const rows = try history.relationRows(std.testing.allocator, committed.entries[0].hash); defer version.freeRelationRows(std.testing.allocator, rows); try std.testing.expectEqual(@as(usize, 0), rows.len);}test "connection open seeds first write from committed database value" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "connection-seed-open.db", .wal = "connection-seed-open.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 960 }); var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "connection-seed-open.history", .recovery = .reject }); defer history.deinit(); var author = try Connection.create(std.testing.allocator, &database, &history, .{}); defer author.deinit(); try executeStatement(&author, "CREATE TABLE items (name)"); try executeStatement(&author, "CREATE TABLE users (name)"); try executeStatement(&author, "INSERT INTO items (rowid, name) VALUES (1, 'base')"); try author.stage(); const base_commit = try author.commit(&history); var reopened = try Connection.open(std.testing.allocator, &database, &history, .{}); defer reopened.deinit(); try std.testing.expect(version.same(base_commit, (try reopened.checkout()).head)); const seeded_root = reopened.session.workingRoot(); try std.testing.expect(version.same((try reopened.workingRoot()), seeded_root.hash)); const seeded_items_hash = testEntryHash(seeded_root, "items"); var live_items = try reopened.catalog.openRelation(std.testing.allocator, "items"); defer live_items.deinit(); _ = try live_items.relation.put(std.testing.allocator, 9, &.{.{ .text = "live-only" }}, .{ .durability = .buffered }); var live_root = try version.databaseRootMaintained( std.testing.allocator, &reopened.catalog, version.ConflictRoot.empty().hash, ); defer live_root.deinit(); try std.testing.expect(!version.same(seeded_items_hash, testEntryHash(&live_root, "items"))); try executeStatement(&reopened, "INSERT INTO users (rowid, name) VALUES (2, 'ada')"); const flushed_root = reopened.session.workingRoot(); try std.testing.expect(version.same(seeded_items_hash, testEntryHash(flushed_root, "items"))); try std.testing.expect(version.same(testEntryHash(&live_root, "users"), testEntryHash(flushed_root, "users")) == false);}test "connection manages branch checkout fast forward and merge commits" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "connection-branch.db", .wal = "connection-branch.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 960 }); var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "connection-branch.history", .recovery = .reject }); defer history.deinit(); var connection = try Connection.create(std.testing.allocator, &database, &history, .{}); defer connection.deinit(); try executeStatement(&connection, "CREATE TABLE items (name)"); try executeStatement(&connection, "INSERT INTO items (rowid, name) VALUES (1, 'base')"); try connection.stage(); const base_commit = try connection.commit(&history); _ = try connection.createBranch(&history, "side"); _ = try connection.createBranch(&history, "behind"); try executeStatement(&connection, "INSERT INTO items (rowid, name) VALUES (2, 'main')"); try connection.stage(); const main_commit = try connection.commit(&history); try std.testing.expect(version.same(base_commit, (try connection.mergeBase(std.testing.allocator, &history, "side")).?)); try connection.checkoutBranch(std.testing.allocator, &history, "side"); try std.testing.expect(version.same(base_commit, (try connection.checkout()).head)); var main_row_after_side_checkout = try connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 2", .{ .durability = .buffered }); defer main_row_after_side_checkout.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 0), main_row_after_side_checkout.rowCount()); try executeStatement(&connection, "INSERT INTO items (rowid, name) VALUES (3, 'side')"); try connection.stage(); const side_commit = try connection.commit(&history); try std.testing.expect(version.same(base_commit, (try connection.mergeBase(std.testing.allocator, &history, "main")).?)); try executeStatement(&connection, "INSERT INTO items (rowid, name) VALUES (4, 'merged')"); try connection.stage(); const merge_commit = try connection.mergeCommit(&history, main_commit); const entries = try history.commitEntries(std.testing.allocator); defer std.testing.allocator.free(entries); var merge_entry: ?branch_mod.CommitEntry = null; for (entries) |entry| { if (version.same(entry.hash, merge_commit)) merge_entry = entry; } try std.testing.expect(merge_entry != null); try std.testing.expectEqual(@as(usize, 2), merge_entry.?.parents.len); try std.testing.expect(version.same(side_commit, merge_entry.?.parents[0])); try std.testing.expect(version.same(main_commit, merge_entry.?.parents[1])); try connection.checkoutBranch(std.testing.allocator, &history, "behind"); try connection.fastForwardBranch(std.testing.allocator, &history, merge_commit); try std.testing.expect(version.same(merge_commit, (try connection.checkout()).head)); try std.testing.expect(version.same((try history.ref("behind")).?.target, merge_commit)); var side_row_after_fast_forward = try connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 3", .{ .durability = .buffered }); defer side_row_after_fast_forward.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), side_row_after_fast_forward.rowCount());}test "connection checkout creates missing relations from committed schema descriptors" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var author_database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "checkout-create-author.db", .wal = "checkout-create-author.wal" }, .header = testingHeader(), }); defer author_database.deinit(); try author_database.reserve(.{ .wal_frames = 960 }); var target_database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "checkout-create-target.db", .wal = "checkout-create-target.wal" }, .header = .{ .sequence = 3904, .salt = .{ .first = 0x1357_3904, .second = 0x2468_3904 }, }, }); defer target_database.deinit(); try target_database.reserve(.{ .wal_frames = 960 }); var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "checkout-create.history", .recovery = .reject }); defer history.deinit(); var author = try Connection.create(std.testing.allocator, &author_database, &history, .{}); defer author.deinit(); _ = try author.createBranch(&history, "side"); try author.checkoutBranch(std.testing.allocator, &history, "side"); try executeStatement(&author, "CREATE TABLE notes (title DEFAULT 'untitled' COLLATE nocase, body, INDEX notes_title (title))"); try executeStatement(&author, "INSERT INTO notes (rowid, title, body) VALUES (9, 'First', 'side note')"); try author.stage(); const side_commit = try author.commit(&history); var target = try Connection.open(std.testing.allocator, &target_database, &history, .{}); defer target.deinit(); try target.checkoutBranch(std.testing.allocator, &history, "side"); try std.testing.expect(version.same(side_commit, (try target.checkout()).head)); try std.testing.expect(version.same((try history.ref("side")).?.target, (try target.checkout()).head)); var found = try target.execute(std.testing.allocator, "SELECT body FROM notes WHERE title = 'first'", .{ .durability = .buffered }); defer found.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), found.rowCount()); const found_view = try row.View.init(found.nextRow().?); try std.testing.expectEqualStrings("side note", (try found_view.column(0)).text); try executeStatement(&target, "INSERT INTO notes (rowid, body) VALUES (10, 'uses default')"); var defaulted = try target.execute(std.testing.allocator, "SELECT title FROM notes WHERE rowid = 10", .{ .durability = .buffered }); defer defaulted.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), defaulted.rowCount()); const defaulted_view = try row.View.init(defaulted.nextRow().?); try std.testing.expectEqualStrings("untitled", (try defaulted_view.column(0)).text);}test "connection checkout recreates stats for missing analyzed relations" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var author_database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "checkout-stats-author.db", .wal = "checkout-stats-author.wal" }, .header = testingHeader(), }); defer author_database.deinit(); try author_database.reserve(.{ .wal_frames = 1280 }); var target_database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "checkout-stats-target.db", .wal = "checkout-stats-target.wal" }, .header = .{ .sequence = 4904, .salt = .{ .first = 0x1357_4904, .second = 0x2468_4904 }, }, }); defer target_database.deinit(); try target_database.reserve(.{ .wal_frames = 1280 }); var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "checkout-stats.history", .recovery = .reject }); defer history.deinit(); var author = try Connection.create(std.testing.allocator, &author_database, &history, .{}); defer author.deinit(); _ = try author.createBranch(&history, "side"); try author.checkoutBranch(std.testing.allocator, &history, "side"); try executeStatement(&author, "CREATE TABLE notes (title COLLATE nocase, body, INDEX notes_title (title))"); try executeStatement(&author, "INSERT INTO notes (rowid, title, body) VALUES (1, 'Alpha', 'first')"); try executeStatement(&author, "INSERT INTO notes (rowid, title, body) VALUES (2, 'Beta', 'second')"); try executeStatement(&author, "INSERT INTO notes (rowid, title, body) VALUES (3, 'Beta', 'third')"); try executeStatement(&author, "ANALYZE notes"); try author.stage(); const side_commit = try author.commit(&history); var target = try Connection.open(std.testing.allocator, &target_database, &history, .{}); defer target.deinit(); try target.checkoutBranch(std.testing.allocator, &history, "side"); try std.testing.expect(version.same(side_commit, (try target.checkout()).head)); var stats = (try target.catalog.relationStats(std.testing.allocator, "notes")).?; defer stats.deinit(); try std.testing.expectEqual(@as(usize, 3), stats.table.entries); try std.testing.expectEqual(@as(usize, 1), stats.indexes.len); const index_stats = stats.index("notes_title").?; try std.testing.expectEqual(@as(usize, 3), index_stats.summary.entries); try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.distinct_values); try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.max_equal); var found = try target.execute(std.testing.allocator, "SELECT body FROM notes WHERE title = 'beta'", .{ .durability = .buffered }); defer found.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 2), found.rowCount());}test "connection checkout drops relations absent from target root" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "checkout-drop.db", .wal = "checkout-drop.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 960 }); var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "checkout-drop.history", .recovery = .reject }); defer history.deinit(); var connection = try Connection.create(std.testing.allocator, &database, &history, .{}); defer connection.deinit(); try executeStatement(&connection, "CREATE TABLE items (name)"); try executeStatement(&connection, "INSERT INTO items (rowid, name) VALUES (7, 'main')"); try connection.stage(); const main_commit = try connection.commit(&history); _ = try connection.createBranch(&history, "side"); try connection.checkoutBranch(std.testing.allocator, &history, "side"); try executeStatement(&connection, "CREATE TABLE notes (title, INDEX notes_title (title))"); try executeStatement(&connection, "INSERT INTO notes (rowid, title) VALUES (1, 'side')"); try connection.stage(); const side_commit = try connection.commit(&history); try std.testing.expect(version.same(side_commit, (try connection.checkout()).head)); var names_on_side = try connection.catalog.relationNames(std.testing.allocator); defer names_on_side.deinit(); try std.testing.expectEqual(@as(usize, 2), names_on_side.names.len); try connection.checkoutBranch(std.testing.allocator, &history, "main"); try std.testing.expect(version.same(main_commit, (try connection.checkout()).head)); try std.testing.expectError(error.TableNotFound, connection.execute(std.testing.allocator, "SELECT title FROM notes WHERE rowid = 1", .{ .durability = .buffered })); var item = try connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 7", .{ .durability = .buffered }); defer item.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), item.rowCount()); const item_view = try row.View.init(item.nextRow().?); try std.testing.expectEqualStrings("main", (try item_view.column(0)).text); var names_on_main = try connection.catalog.relationNames(std.testing.allocator); defer names_on_main.deinit(); try std.testing.expectEqual(@as(usize, 1), names_on_main.names.len); try std.testing.expectEqualStrings("items", names_on_main.names[0]); try connection.checkoutBranch(std.testing.allocator, &history, "side"); var found = try connection.execute(std.testing.allocator, "SELECT title FROM notes WHERE rowid = 1", .{ .durability = .buffered }); defer found.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), found.rowCount()); const found_view = try row.View.init(found.nextRow().?); try std.testing.expectEqualStrings("side", (try found_view.column(0)).text);}test "connection drop table statement keeps pre-drop commits readable" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "drop-statement.db", .wal = "drop-statement.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 960 }); var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "drop-statement.history", .recovery = .reject }); defer history.deinit(); var connection = try Connection.create(std.testing.allocator, &database, &history, .{}); defer connection.deinit(); try executeStatement(&connection, "CREATE TABLE items (name)"); try executeStatement(&connection, "INSERT INTO items (rowid, name) VALUES (7, 'kept')"); try connection.stage(); const keep_commit = try connection.commit(&history); _ = try connection.createBranch(&history, "keep"); var dropped = try connection.execute(std.testing.allocator, "DROP TABLE items", .{ .durability = .buffered }); defer dropped.deinit(std.testing.allocator); const drop_flush = switch (dropped) { .catalog => |flush| flush, else => return error.UnsupportedStatement, }; try std.testing.expect(version.same(drop_flush.database, (try connection.workingRoot()))); try std.testing.expect(testFindEntry(connection.session.workingRoot(), "items") == null); try std.testing.expectError(error.TableNotFound, connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 7", .{ .durability = .buffered })); try connection.stage(); const drop_commit = try connection.commit(&history); var dropped_root = try history.commitDatabaseRoot(std.testing.allocator, drop_commit); defer dropped_root.deinit(); try std.testing.expectEqual(@as(usize, 0), dropped_root.entries.len); var kept_root = try history.commitDatabaseRoot(std.testing.allocator, keep_commit); defer kept_root.deinit(); try std.testing.expectEqual(@as(usize, 1), kept_root.entries.len); try std.testing.expectEqualStrings("items", kept_root.entries[0].name); var names_after_drop = try connection.catalog.relationNames(std.testing.allocator); defer names_after_drop.deinit(); try std.testing.expectEqual(@as(usize, 0), names_after_drop.names.len); try connection.checkoutBranch(std.testing.allocator, &history, "keep"); try std.testing.expect(version.same(keep_commit, (try connection.checkout()).head)); var restored = try connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 7", .{ .durability = .buffered }); defer restored.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), restored.rowCount()); const restored_view = try row.View.init(restored.nextRow().?); try std.testing.expectEqualStrings("kept", (try restored_view.column(0)).text);}test "connection checkout discards queued relation session edits" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "checkout-session-discard.db", .wal = "checkout-session-discard.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 960 }); var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "checkout-session-discard.history", .recovery = .reject }); defer history.deinit(); var connection = try Connection.create(std.testing.allocator, &database, &history, .{}); defer connection.deinit(); try executeStatement(&connection, "CREATE TABLE items (name)"); try connection.stage(); const base_commit = try connection.commit(&history); _ = try connection.createBranch(&history, "side"); var prepared = try plan_mod.prepareRelation(&connection.catalog, std.testing.allocator, "items", plan_mod.emptyParameterShape()); var relation_session = try prepared.writeSession(); var relation_session_live = true; errdefer if (relation_session_live) relation_session.deinit(); try relation_session.put(42, &.{.{ .text = "queued" }}); const write_limits = try relation_session.stagingLimits(); var workspace = try session_mod.DatabaseWrite.Workspace.allocate( std.testing.allocator, write_limits, ); defer workspace.deallocate(std.testing.allocator); var write = try connection.beginWrite(&workspace, std.testing.allocator, .{ .limits = write_limits, .durability = .buffered, }); try write.stageRelation(&relation_session); relation_session_live = false; try std.testing.expectEqual(@as(usize, 1), write.pendingRelations()); prepared.deinit(); try connection.checkoutBranch(std.testing.allocator, &history, "side"); try std.testing.expect(version.same(base_commit, (try connection.checkout()).head)); try std.testing.expectEqual(@as(usize, 0), connection.session.pendingRelations()); const empty_limits = session_mod.DatabaseWrite.Limits{ .relations = 0, .edits = 0, .payload_bytes = 0, .assignments = 0, }; var replacement_write = try connection.beginWrite(&workspace, std.testing.allocator, .{ .limits = empty_limits, .durability = .buffered, }); defer replacement_write.deinit(); write.deinit(); try std.testing.expectError( error.WriteSessionActive, connection.beginWrite(&workspace, std.testing.allocator, .{ .limits = empty_limits, .durability = .buffered, }), ); var queued = try connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 42", .{ .durability = .buffered }); defer queued.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 0), queued.rowCount());}test "connection write session stages statements before one root flush" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "connection-write-session.db", .wal = "connection-write-session.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 960 }); var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "connection-write-session.history", .recovery = .reject }); defer history.deinit(); var connection = try Connection.create(std.testing.allocator, &database, &history, .{}); defer connection.deinit(); try executeStatement(&connection, "CREATE TABLE items (name)"); const before = (try connection.workingRoot()); var insert = try connection.prepare(std.testing.allocator, "INSERT INTO items (rowid, name) VALUES (?1, ?2)"); const write_options = WriteOptions{ .limits = .{ .relations = 1, .edits = 2, .payload_bytes = 32, .assignments = 0, }, .durability = .buffered, }; var workspace = try session_mod.DatabaseWrite.Workspace.allocate( std.testing.allocator, write_options.limits, ); defer workspace.deallocate(std.testing.allocator); var write = try connection.beginWrite( &workspace, std.testing.allocator, write_options, ); defer write.deinit(); try std.testing.expectError( error.WriteSessionActive, connection.beginWrite(&workspace, std.testing.allocator, write_options), ); try insert.bind(1, .{ .integer = 1 }); try insert.bind(2, .{ .text = "one" }); var first = try connection.executePrepared(&insert, std.testing.allocator, .{ .durability = .buffered, .write = &write }); defer first.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), first.staged); try insert.bind(1, .{ .integer = 2 }); try insert.bind(2, .{ .text = "two" }); var second = try connection.executePrepared(&insert, std.testing.allocator, .{ .durability = .buffered, .write = &write }); defer second.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), second.staged); insert.deinit(); try std.testing.expect(version.same(before, (try connection.workingRoot()))); var before_flush = try connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid >= 1", .{ .durability = .buffered }); defer before_flush.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 0), before_flush.rowCount()); var flush = try write.flush(); defer flush.deinit(); try std.testing.expectEqual(@as(usize, 1), flush.relations.len); try std.testing.expect(version.same(flush.database, (try connection.workingRoot()))); try std.testing.expect(!version.same(before, (try connection.workingRoot()))); const working_root = connection.session.workingRoot(); try std.testing.expect(version.same(flush.database, working_root.hash)); try std.testing.expectEqual(@as(usize, 1), working_root.entries.len); try std.testing.expectError(error.DatabaseRootNotFound, history.databaseRoot(std.testing.allocator, flush.database)); var after_flush = try connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid >= 1", .{ .durability = .buffered }); defer after_flush.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 2), after_flush.rowCount()); try connection.stage(); const commit_hash = try connection.commit(&history); try std.testing.expect(version.same(flush.database, connection.session.workingRoot().hash)); var committed = try history.commitDatabaseRoot(std.testing.allocator, commit_hash); defer committed.deinit(); try std.testing.expect(version.same(flush.database, committed.hash)); const rows = try history.relationRows(std.testing.allocator, flush.relations[0].relation); defer version.freeRelationRows(std.testing.allocator, rows); try std.testing.expectEqual(@as(usize, 2), rows.len);}test "connection diffs relation views without raw catalog access" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var left = TestingConnection{}; try left.init(std.testing.allocator, tmp.dir, "left.db", "left.wal", "left.history"); defer left.deinit(); var right = TestingConnection{}; try right.init(std.testing.allocator, tmp.dir, "right.db", "right.wal", "right.history"); defer right.deinit(); try executeStatement(&left.connection, "CREATE TABLE items (name)"); try executeStatement(&left.connection, "INSERT INTO items (rowid, name) VALUES (1, 'same')"); try executeStatement(&left.connection, "INSERT INTO items (rowid, name) VALUES (2, 'old')"); try executeStatement(&right.connection, "CREATE TABLE items (name)"); try executeStatement(&right.connection, "INSERT INTO items (rowid, name) VALUES (1, 'same')"); try executeStatement(&right.connection, "INSERT INTO items (rowid, name) VALUES (2, 'new')"); try executeStatement(&right.connection, "INSERT INTO items (rowid, name) VALUES (3, 'added')"); var right_view = try right.connection.relationView(std.testing.allocator, "items"); defer right_view.deinit(); var result = try left.connection.diffRelation(std.testing.allocator, "items", right_view.snapshot()); defer result.deinit(); try std.testing.expect(!result.schema_changed); try std.testing.expectEqual(@as(usize, 2), result.changes.len); try std.testing.expectEqual(diff_mod.ChangeKind.modified, result.changes[0].kind); try std.testing.expectEqual(@as(i64, 2), result.changes[0].rowid); try std.testing.expectEqual(diff_mod.ChangeKind.added, result.changes[1].kind); try std.testing.expectEqual(@as(i64, 3), result.changes[1].rowid);}test "connection applies explicit database merge snapshots into working root" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var base = TestingConnection{}; try base.init(std.testing.allocator, tmp.dir, "merge-base.db", "merge-base.wal", "merge-base.history"); defer base.deinit(); var ours = TestingConnection{}; try ours.init(std.testing.allocator, tmp.dir, "merge-ours.db", "merge-ours.wal", "merge-ours.history"); defer ours.deinit(); var theirs = TestingConnection{}; try theirs.init(std.testing.allocator, tmp.dir, "merge-theirs.db", "merge-theirs.wal", "merge-theirs.history"); defer theirs.deinit(); try createItems(&base.connection); try executeStatement(&base.connection, "INSERT INTO items (rowid, name) VALUES (1, 'same')"); try executeStatement(&base.connection, "INSERT INTO items (rowid, name) VALUES (2, 'base')"); try executeStatement(&base.connection, "INSERT INTO items (rowid, name) VALUES (3, 'delete')"); try createItems(&ours.connection); try executeStatement(&ours.connection, "INSERT INTO items (rowid, name) VALUES (1, 'same')"); try executeStatement(&ours.connection, "INSERT INTO items (rowid, name) VALUES (2, 'ours')"); try executeStatement(&ours.connection, "INSERT INTO items (rowid, name) VALUES (3, 'delete')"); try executeStatement(&ours.connection, "INSERT INTO items (rowid, name) VALUES (5, 'ours-add')"); try executeStatement(&ours.connection, "ANALYZE items"); var ours_stats = (try ours.connection.catalog.relationStats(std.testing.allocator, "items")).?; ours_stats.deinit(); try createItems(&theirs.connection); try executeStatement(&theirs.connection, "INSERT INTO items (rowid, name) VALUES (1, 'same')"); try executeStatement(&theirs.connection, "INSERT INTO items (rowid, name) VALUES (2, 'base')"); try executeStatement(&theirs.connection, "INSERT INTO items (rowid, name) VALUES (6, 'theirs-add')"); const names = [_][]const u8{"items"}; var base_view = try base.connection.databaseView(std.testing.allocator, &base.history, names[0..]); defer base_view.deinit(); var theirs_view = try theirs.connection.databaseView(std.testing.allocator, &theirs.history, names[0..]); defer theirs_view.deinit(); const previous_root = (try ours.connection.workingRoot()); var merged = try ours.connection.mergeDatabase(std.testing.allocator, &ours.history, base_view.snapshot(), theirs_view.snapshot(), .{}); defer merged.deinit(); try std.testing.expect(!merged.hasConflicts()); try std.testing.expect(!version.same(previous_root, (try ours.connection.workingRoot()))); try std.testing.expect(version.same(merged.value.root.hash, (try ours.connection.workingRoot()))); try std.testing.expect(version.same(merged.value.root.hash, ours.connection.session.workingRoot().hash)); try ours.history.validateConflictRoot(merged.value.root.conflicts); try expectLiveDatabaseRoot(&ours.connection, &merged.value.root); const merged_relation = merged.value.findRelation("items").?; try std.testing.expect(version.same(merged_relation.root.stats.hash, version.emptyHash("sql.stats.none"))); try std.testing.expect((try ours.connection.catalog.relationStats(std.testing.allocator, "items")) == null); try std.testing.expect((try ours.connection.checkout()).working.dirty()); try std.testing.expectError(error.DatabaseRootNotFound, ours.history.databaseRoot(std.testing.allocator, merged.value.root.hash)); var found = try ours.connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 6", .{ .durability = .buffered }); defer found.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), found.rowCount()); const added_view = try row.View.init(found.nextRow().?); try std.testing.expectEqualStrings("theirs-add", (try added_view.column(0)).text); var missing = try ours.connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 3", .{ .durability = .buffered }); defer missing.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 0), missing.rowCount()); try ours.connection.stage(); const merge_commit = try ours.connection.commit(&ours.history); var committed_value = try ours.history.databaseValue(std.testing.allocator, (try ours.connection.workingRoot())); defer committed_value.deinit(); try std.testing.expect(version.same(merge_commit, (try ours.connection.checkout()).head)); try std.testing.expect(version.same(merged.value.root.hash, committed_value.root.hash));}test "connection rejects a selected schema before changing the working root" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var base = TestingConnection{}; try base.init(std.testing.allocator, tmp.dir, "schema-base.db", "schema-base.wal", "schema-base.history"); defer base.deinit(); var ours = TestingConnection{}; try ours.init(std.testing.allocator, tmp.dir, "schema-ours.db", "schema-ours.wal", "schema-ours.history"); defer ours.deinit(); var theirs = TestingConnection{}; try theirs.init(std.testing.allocator, tmp.dir, "schema-theirs.db", "schema-theirs.wal", "schema-theirs.history"); defer theirs.deinit(); try createItems(&base.connection); try createItems(&ours.connection); try createItems(&theirs.connection); try executeStatement(&base.connection, "CREATE TABLE alpha (name)"); try executeStatement(&ours.connection, "CREATE TABLE alpha (name)"); try executeStatement(&theirs.connection, "CREATE TABLE alpha (name)"); try executeStatement(&base.connection, "INSERT INTO alpha (rowid, name) VALUES (1, 'base')"); try executeStatement(&ours.connection, "INSERT INTO alpha (rowid, name) VALUES (1, 'base')"); try executeStatement(&theirs.connection, "INSERT INTO alpha (rowid, name) VALUES (1, 'theirs')"); try executeStatement(&theirs.connection, "CREATE INDEX items_name ON items (name)"); const names = [_][]const u8{ "alpha", "items" }; var base_view = try base.connection.databaseView(std.testing.allocator, &base.history, names[0..]); defer base_view.deinit(); var theirs_view = try theirs.connection.databaseView(std.testing.allocator, &theirs.history, names[0..]); defer theirs_view.deinit(); const previous_root = (try ours.connection.workingRoot()); try std.testing.expectError(error.UnsupportedCheckoutRoot, ours.connection.mergeDatabase( std.testing.allocator, &ours.history, base_view.snapshot(), theirs_view.snapshot(), .{}, )); try std.testing.expect(version.same(previous_root, (try ours.connection.workingRoot()))); var items = try ours.connection.relationView(std.testing.allocator, "items"); defer items.deinit(); try std.testing.expectEqual(@as(usize, 0), items.root.indexes.len); var alpha = try ours.connection.execute(std.testing.allocator, "SELECT name FROM alpha WHERE rowid = 1", .{ .durability = .buffered }); defer alpha.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), alpha.rowCount()); const alpha_view = try row.View.init(alpha.nextRow().?); try std.testing.expectEqualStrings("base", (try alpha_view.column(0)).text);}test "connection replaces conflict root after value resolution" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var base = TestingConnection{}; try base.init(std.testing.allocator, tmp.dir, "resolve-base.db", "resolve-base.wal", "resolve-base.history"); defer base.deinit(); var ours = TestingConnection{}; try ours.init(std.testing.allocator, tmp.dir, "resolve-ours.db", "resolve-ours.wal", "resolve-ours.history"); defer ours.deinit(); var theirs = TestingConnection{}; try theirs.init(std.testing.allocator, tmp.dir, "resolve-theirs.db", "resolve-theirs.wal", "resolve-theirs.history"); defer theirs.deinit(); try createItems(&base.connection); try executeStatement(&base.connection, "INSERT INTO items (rowid, name) VALUES (1, 'base')"); try createItems(&ours.connection); try executeStatement(&ours.connection, "INSERT INTO items (rowid, name) VALUES (1, 'ours')"); try createItems(&theirs.connection); try executeStatement(&theirs.connection, "INSERT INTO items (rowid, name) VALUES (1, 'theirs')"); const names = [_][]const u8{"items"}; var base_view = try base.connection.databaseView(std.testing.allocator, &base.history, names[0..]); defer base_view.deinit(); var theirs_view = try theirs.connection.databaseView(std.testing.allocator, &theirs.history, names[0..]); defer theirs_view.deinit(); var merged = try ours.connection.mergeDatabase(std.testing.allocator, &ours.history, base_view.snapshot(), theirs_view.snapshot(), .{}); defer merged.deinit(); try std.testing.expect(merged.hasConflicts()); try std.testing.expectEqual(@as(usize, 1), merged.conflict_root.count); try std.testing.expect(version.same(merged.conflict_root.hash, ours.connection.session.workingRoot().conflicts)); try ours.history.validateConflictRoot(merged.value.root.conflicts); try expectLiveDatabaseRoot(&ours.connection, &merged.value.root); var neutral_root = try version.databaseRoot( std.testing.allocator, &ours.connection.catalog, version.ConflictRoot.empty().hash, ); defer neutral_root.deinit(); try std.testing.expect(!version.same(neutral_root.hash, merged.value.root.hash)); var persisted_entries = try ours.history.conflictEntries(std.testing.allocator, merged.conflict_root.hash); defer persisted_entries.deinit(); try std.testing.expectEqual(@as(usize, 1), persisted_entries.entries.len); const conflicted_root = (try ours.connection.workingRoot()); const clean_root = try ours.connection.replaceConflictRoot(version.ConflictRoot.empty()); try std.testing.expect(!version.same(conflicted_root, clean_root)); try std.testing.expect(version.same(version.ConflictRoot.empty().hash, ours.connection.session.workingRoot().conflicts)); var selected = try ours.connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 1", .{ .durability = .buffered }); defer selected.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), selected.rowCount()); const selected_view = try row.View.init(selected.nextRow().?); try std.testing.expectEqualStrings("ours", (try selected_view.column(0)).text); try ours.connection.stage(); const commit_hash = try ours.connection.commit(&ours.history); try std.testing.expect(version.same(commit_hash, (try ours.connection.checkout()).head)); var committed = try ours.history.databaseValue(std.testing.allocator, clean_root); defer committed.deinit(); try std.testing.expect(version.same(clean_root, committed.root.hash)); try std.testing.expect(version.same(version.ConflictRoot.empty().hash, committed.root.conflicts));}test "connection merge materialization rejects mismatched and unclosed conflicts" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var store = TestingConnection{}; try store.init( std.testing.allocator, tmp.dir, "merge-conflict-closure.db", "merge-conflict-closure.wal", "merge-conflict-closure.history", ); defer store.deinit(); try createItems(&store.connection); try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'ours')"); const baseline = (try store.connection.workingRoot()); const artifact = version.ConflictArtifact.init("items", 1, "base", "ours", "theirs"); const conflicts = version.ConflictRoot.init(&.{artifact.entry()}); var value = try store.connection.materializedWorkingValue(std.testing.allocator); const rooted = try value.root.withConflicts(std.testing.allocator, conflicts.hash); value.root.deinit(); value.root = rooted; var artifacts = [_]version.ConflictArtifact{artifact}; var result = merge_mod.DatabaseMerge{ .allocator = std.testing.allocator, .value = value, .conflict_root = version.ConflictRoot.empty(), .relations = &.{}, .discovered = &.{}, .artifacts = artifacts[0..], }; defer result.value.deinit(); try std.testing.expectError( error.InvalidHistory, store.connection.materializeDatabaseMerge( std.testing.allocator, &store.history, &result, .{ .durability = .buffered }, ), ); try std.testing.expect(version.same(baseline, (try store.connection.workingRoot()))); result.conflict_root = conflicts; try store.history.putConflict(artifact); store.history.findConflict(artifact.hash).?.artifact.rowid += 1; try std.testing.expectError( error.InvalidHistory, store.connection.materializeDatabaseMerge( std.testing.allocator, &store.history, &result, .{ .durability = .buffered }, ), ); try std.testing.expect(version.same(baseline, (try store.connection.workingRoot())));}test "connection materialized root verifier rejects relation identity drift" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var store = TestingConnection{}; try store.init(std.testing.allocator, tmp.dir, "merge-relation-drift.db", "merge-relation-drift.wal", "merge-relation-drift.history"); defer store.deinit(); try createItems(&store.connection); try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'target')"); var target = try store.connection.materializedWorkingValue(std.testing.allocator); defer target.deinit(); try executeStatement(&store.connection, "UPDATE items SET name = 'drift' WHERE rowid = 1"); try std.testing.expectError( error.InvalidHistory, store.connection.verifyMaterializedDatabaseRoot(std.testing.allocator, &target.root), ); var live = try version.databaseRoot( std.testing.allocator, &store.connection.catalog, target.root.conflicts, ); defer live.deinit(); try std.testing.expect(!version.same(live.hash, target.root.hash));}test "connection materialized root verifier rejects an extra live relation" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var store = TestingConnection{}; try store.init(std.testing.allocator, tmp.dir, "merge-extra-relation.db", "merge-extra-relation.wal", "merge-extra-relation.history"); defer store.deinit(); try createItems(&store.connection); try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'target')"); var target = try store.connection.materializedWorkingValue(std.testing.allocator); defer target.deinit(); try executeStatement(&store.connection, "CREATE TABLE untracked (value)"); const entry = relationEntry(target.root.entries, "items").?; var handle = try store.connection.catalog.openRelation(std.testing.allocator, "items"); defer handle.deinit(); var stats = try store.connection.catalog.relationStats(std.testing.allocator, "items"); defer if (stats) |*relation_stats| relation_stats.deinit(); const live = try version.relationKey( "items", &handle, if (stats) |*relation_stats| relation_stats else null, ); try std.testing.expect(version.same(live.hash, entry.hash)); try std.testing.expectError( error.InvalidHistory, store.connection.verifyMaterializedDatabaseRoot(std.testing.allocator, &target.root), );}test "connection resolves selected conflict artifacts" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var base = TestingConnection{}; try base.init(std.testing.allocator, tmp.dir, "resolve-selected-base.db", "resolve-selected-base.wal", "resolve-selected-base.history"); defer base.deinit(); var ours = TestingConnection{}; try ours.init(std.testing.allocator, tmp.dir, "resolve-selected-ours.db", "resolve-selected-ours.wal", "resolve-selected-ours.history"); defer ours.deinit(); var theirs = TestingConnection{}; try theirs.init(std.testing.allocator, tmp.dir, "resolve-selected-theirs.db", "resolve-selected-theirs.wal", "resolve-selected-theirs.history"); defer theirs.deinit(); try createItems(&base.connection); try executeStatement(&base.connection, "INSERT INTO items (rowid, name) VALUES (1, 'base-one')"); try executeStatement(&base.connection, "INSERT INTO items (rowid, name) VALUES (2, 'base-two')"); try createItems(&ours.connection); try executeStatement(&ours.connection, "INSERT INTO items (rowid, name) VALUES (1, 'ours-one')"); try executeStatement(&ours.connection, "INSERT INTO items (rowid, name) VALUES (2, 'ours-two')"); try createItems(&theirs.connection); try executeStatement(&theirs.connection, "INSERT INTO items (rowid, name) VALUES (1, 'theirs-one')"); try executeStatement(&theirs.connection, "INSERT INTO items (rowid, name) VALUES (2, 'theirs-two')"); const names = [_][]const u8{"items"}; var base_view = try base.connection.databaseView(std.testing.allocator, &base.history, names[0..]); defer base_view.deinit(); var theirs_view = try theirs.connection.databaseView(std.testing.allocator, &theirs.history, names[0..]); defer theirs_view.deinit(); var merged = try ours.connection.mergeDatabase(std.testing.allocator, &ours.history, base_view.snapshot(), theirs_view.snapshot(), .{}); defer merged.deinit(); try std.testing.expect(merged.hasConflicts()); try std.testing.expectEqual(@as(usize, 2), merged.conflict_root.count); var artifacts = try ours.connection.conflictArtifacts(std.testing.allocator, &ours.history); defer artifacts.deinit(); try std.testing.expect(version.same(merged.conflict_root.hash, artifacts.root.hash)); try std.testing.expectEqual(@as(usize, 2), artifacts.artifacts.len); try std.testing.expectEqual(@as(i64, 1), artifacts.artifacts[0].rowid); try std.testing.expect(artifacts.artifacts[0].base != null); try std.testing.expect(artifacts.artifacts[0].ours != null); try std.testing.expect(artifacts.artifacts[0].theirs != null); try std.testing.expectEqual(@as(i64, 2), artifacts.artifacts[1].rowid); try std.testing.expect(artifacts.artifacts[1].base != null); try std.testing.expect(artifacts.artifacts[1].ours != null); try std.testing.expect(artifacts.artifacts[1].theirs != null); const conflicted_root = (try ours.connection.workingRoot()); const unchanged_root = try ours.connection.resolveConflicts(std.testing.allocator, &ours.history, &.{}); try std.testing.expect(version.same(conflicted_root, unchanged_root)); const missing = version.emptyHash("sql.conflict.missing"); try std.testing.expectError(error.ConflictNotFound, ours.connection.resolveConflicts(std.testing.allocator, &ours.history, &.{missing})); try std.testing.expect(version.same(conflicted_root, (try ours.connection.workingRoot()))); const first_hash = artifacts.artifacts[0].hash; const remaining_hash = artifacts.artifacts[1].hash; const partially_clean_root = try ours.connection.resolveConflicts(std.testing.allocator, &ours.history, &.{first_hash}); try std.testing.expect(!version.same(conflicted_root, partially_clean_root)); var remaining_artifacts = try ours.connection.conflictArtifacts(std.testing.allocator, &ours.history); defer remaining_artifacts.deinit(); try std.testing.expectEqual(@as(usize, 1), remaining_artifacts.artifacts.len); try std.testing.expectEqual(@as(i64, 2), remaining_artifacts.artifacts[0].rowid); try std.testing.expect(version.same(remaining_hash, remaining_artifacts.artifacts[0].hash)); const clean_root = try ours.connection.resolveConflicts(std.testing.allocator, &ours.history, &.{remaining_hash}); try std.testing.expect(!version.same(partially_clean_root, clean_root)); try std.testing.expect(version.same(version.ConflictRoot.empty().hash, ours.connection.session.workingRoot().conflicts)); var selected_one = try ours.connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 1", .{ .durability = .buffered }); defer selected_one.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), selected_one.rowCount()); const selected_one_view = try row.View.init(selected_one.nextRow().?); try std.testing.expectEqualStrings("ours-one", (try selected_one_view.column(0)).text); var selected_two = try ours.connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 2", .{ .durability = .buffered }); defer selected_two.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), selected_two.rowCount()); const selected_two_view = try row.View.init(selected_two.nextRow().?); try std.testing.expectEqualStrings("ours-two", (try selected_two_view.column(0)).text); try ours.connection.stage(); const commit_hash = try ours.connection.commit(&ours.history); try std.testing.expect(version.same(commit_hash, (try ours.connection.checkout()).head)); var committed = try ours.history.databaseValue(std.testing.allocator, clean_root); defer committed.deinit(); try std.testing.expect(version.same(clean_root, committed.root.hash)); try std.testing.expect(version.same(version.ConflictRoot.empty().hash, committed.root.conflicts));}test "connection merge commit records parent when resolved root matches ours" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var store = TestingConnection{}; try store.init(std.testing.allocator, tmp.dir, "merge-same-root.db", "merge-same-root.wal", "merge-same-root.history"); defer store.deinit(); try createItems(&store.connection); try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'base')"); try store.connection.stage(); _ = try store.connection.commit(&store.history); _ = try store.connection.createBranch(&store.history, "side"); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "side"); try executeStatement(&store.connection, "DELETE FROM items WHERE rowid = 1"); try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'theirs')"); try store.connection.stage(); const side_commit = try store.connection.commit(&store.history); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main"); try executeStatement(&store.connection, "DELETE FROM items WHERE rowid = 1"); try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'ours')"); try store.connection.stage(); const ours_commit = try store.connection.commit(&store.history); const ours_root = (try store.connection.workingRoot()); var merged = try store.connection.mergeBranch(std.testing.allocator, &store.history, "side", .{}); defer merged.deinit(); try std.testing.expect(merged.hasConflicts()); var artifacts = try store.connection.conflictArtifacts(std.testing.allocator, &store.history); defer artifacts.deinit(); try std.testing.expectEqual(@as(usize, 1), artifacts.artifacts.len); _ = try store.connection.resolveConflicts(std.testing.allocator, &store.history, &.{artifacts.artifacts[0].hash}); try std.testing.expect(version.same(ours_root, (try store.connection.workingRoot()))); const merge_commit = try store.connection.mergeCommit(&store.history, side_commit); const commit = try store.history.commitValue(merge_commit); try std.testing.expect(version.same(ours_root, commit.root)); try std.testing.expectEqual(@as(usize, 2), commit.parents.len); try std.testing.expect(version.same(ours_commit, commit.parents[0])); try std.testing.expect(version.same(side_commit, commit.parents[1]));}test "connection merge materializes independent relation additions" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var base = TestingConnection{}; try base.init(std.testing.allocator, tmp.dir, "merge-add-base.db", "merge-add-base.wal", "merge-add-base.history"); defer base.deinit(); var ours = TestingConnection{}; try ours.init(std.testing.allocator, tmp.dir, "merge-add-ours.db", "merge-add-ours.wal", "merge-add-ours.history"); defer ours.deinit(); var theirs = TestingConnection{}; try theirs.init(std.testing.allocator, tmp.dir, "merge-add-theirs.db", "merge-add-theirs.wal", "merge-add-theirs.history"); defer theirs.deinit(); try createItems(&base.connection); try executeStatement(&base.connection, "INSERT INTO items (rowid, name) VALUES (1, 'base')"); try createItems(&ours.connection); try executeStatement(&ours.connection, "INSERT INTO items (rowid, name) VALUES (1, 'base')"); try executeStatement(&ours.connection, "CREATE TABLE local (body)"); try executeStatement(&ours.connection, "INSERT INTO local (rowid, body) VALUES (7, 'ours-only')"); try createItems(&theirs.connection); try executeStatement(&theirs.connection, "INSERT INTO items (rowid, name) VALUES (1, 'base')"); try executeStatement(&theirs.connection, "CREATE TABLE side (body)"); try executeStatement(&theirs.connection, "INSERT INTO side (rowid, body) VALUES (8, 'theirs-only')"); const base_names = [_][]const u8{"items"}; const theirs_names = [_][]const u8{ "side", "items" }; var base_view = try base.connection.databaseView(std.testing.allocator, &base.history, base_names[0..]); defer base_view.deinit(); var theirs_view = try theirs.connection.databaseView(std.testing.allocator, &theirs.history, theirs_names[0..]); defer theirs_view.deinit(); var merged = try ours.connection.mergeDatabase(std.testing.allocator, &ours.history, base_view.snapshot(), theirs_view.snapshot(), .{}); defer merged.deinit(); try std.testing.expect(!merged.hasConflicts()); try std.testing.expect(merged.value.findRelation("items") != null); try std.testing.expect(merged.value.findRelation("local") != null); try std.testing.expect(merged.value.findRelation("side") != null); try std.testing.expect(version.same(merged.value.root.hash, ours.connection.session.workingRoot().hash)); var local = try ours.connection.execute(std.testing.allocator, "SELECT body FROM local WHERE rowid = 7", .{ .durability = .buffered }); defer local.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), local.rowCount()); const local_view = try row.View.init(local.nextRow().?); try std.testing.expectEqualStrings("ours-only", (try local_view.column(0)).text); var side = try ours.connection.execute(std.testing.allocator, "SELECT body FROM side WHERE rowid = 8", .{ .durability = .buffered }); defer side.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), side.rowCount()); const side_view = try row.View.init(side.nextRow().?); try std.testing.expectEqualStrings("theirs-only", (try side_view.column(0)).text);}test "connection merges branch refs from history snapshots" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{ .paths = .{ .database = "merge-branch.db", .wal = "merge-branch.wal" }, .header = testingHeader(), }); defer database.deinit(); try database.reserve(.{ .wal_frames = 960 }); var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "merge-branch.history", .recovery = .reject }); defer history.deinit(); var main = try Connection.create(std.testing.allocator, &database, &history, .{}); defer main.deinit(); try createItems(&main); try executeStatement(&main, "INSERT INTO items (rowid, name) VALUES (1, 'base')"); try main.stage(); const base_commit = try main.commit(&history); _ = try main.createBranch(&history, "side"); try executeStatement(&main, "INSERT INTO items (rowid, name) VALUES (2, 'main')"); try main.stage(); const main_commit = try main.commit(&history); try main.checkoutBranch(std.testing.allocator, &history, "side"); var main_row_on_side = try main.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 2", .{ .durability = .buffered }); defer main_row_on_side.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 0), main_row_on_side.rowCount()); try executeStatement(&main, "INSERT INTO items (rowid, name) VALUES (3, 'side')"); try main.stage(); const side_commit = try main.commit(&history); try std.testing.expect(version.same(side_commit, (try history.ref("side")).?.target)); try main.checkoutBranch(std.testing.allocator, &history, "main"); try std.testing.expect(version.same(main_commit, (try main.checkout()).head)); try std.testing.expect(version.same(base_commit, (try main.mergeBase(std.testing.allocator, &history, "side")).?)); var side_row_on_main = try main.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 3", .{ .durability = .buffered }); defer side_row_on_main.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 0), side_row_on_main.rowCount()); var main_row_on_main = try main.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 2", .{ .durability = .buffered }); defer main_row_on_main.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), main_row_on_main.rowCount()); const previous_root = (try main.workingRoot()); var merged = try main.mergeBranch(std.testing.allocator, &history, "side", .{}); defer merged.deinit(); try std.testing.expect(!merged.hasConflicts()); try std.testing.expect(!version.same(previous_root, (try main.workingRoot()))); try std.testing.expect(version.same(merged.value.root.hash, (try main.workingRoot()))); try std.testing.expect(version.same(merged.value.root.hash, main.session.workingRoot().hash)); var found = try main.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 3", .{ .durability = .buffered }); defer found.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), found.rowCount()); const found_view = try row.View.init(found.nextRow().?); try std.testing.expectEqualStrings("side", (try found_view.column(0)).text);}test "committed conflict survives identical later merge" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var store = TestingConnection{}; try store.init(std.testing.allocator, tmp.dir, "conflict-carry.db", "conflict-carry.wal", "conflict-carry.history"); defer store.deinit(); const conflict = try commitTestingConflict(&store); try std.testing.expect(version.same(conflict.commit, (try store.connection.checkout()).head)); _ = try store.connection.createBranch(&store.history, "same"); var later = try store.connection.mergeBranch(std.testing.allocator, &store.history, "same", .{}); defer later.deinit(); try std.testing.expect(later.hasConflicts()); try std.testing.expectEqual(@as(usize, 1), later.conflict_root.count); try std.testing.expect(version.same(conflict.root, later.conflict_root.hash)); try std.testing.expect(version.same(conflict.database, (try store.connection.workingRoot()))); try expectTestingConflict(&store, conflict); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "side"); try std.testing.expect(version.same(version.ConflictRoot.empty().hash, store.connection.session.workingRoot().conflicts)); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main"); try std.testing.expect(version.same(conflict.commit, (try store.connection.checkout()).head)); try std.testing.expect(version.same(conflict.database, (try store.connection.workingRoot()))); try expectTestingConflict(&store, conflict); try executeStatement(&store.connection, "ANALYZE items"); try executeStatement(&store.connection, "CREATE TABLE notes (value)"); try executeStatement(&store.connection, "DROP TABLE notes"); try expectTestingConflict(&store, conflict); _ = try store.connection.resolveConflicts(std.testing.allocator, &store.history, &.{conflict.artifact}); try std.testing.expect(version.same(version.ConflictRoot.empty().hash, store.connection.session.workingRoot().conflicts));}test "connection fast forward adopts a committed conflict root" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var store = TestingConnection{}; try store.init(std.testing.allocator, tmp.dir, "conflict-forward.db", "conflict-forward.wal", "conflict-forward.history"); defer store.deinit(); const conflict = try commitTestingConflict(&store); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "behind"); try store.connection.fastForwardBranch(std.testing.allocator, &store.history, conflict.commit); try std.testing.expect(version.same(conflict.commit, (try store.connection.checkout()).head)); try std.testing.expect(version.same(conflict.commit, (try store.history.ref("behind")).?.target)); try std.testing.expect(version.same(conflict.database, (try store.connection.workingRoot()))); try expectTestingConflict(&store, conflict);}test "connection commit rejects an unclosed conflict root" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var store = TestingConnection{}; try store.init( std.testing.allocator, tmp.dir, "conflict-closure.db", "conflict-closure.wal", "conflict-closure.history", ); defer store.deinit(); try createItems(&store.connection); try store.connection.stage(); const baseline = try store.connection.commit(&store.history); const artifact = version.ConflictArtifact.init( "items", 1, "base", "ours", "theirs", ); const conflicts = version.ConflictRoot.init(&.{artifact.entry()}); const invalid_root = try store.connection.replaceConflictRoot(conflicts); try store.connection.stage(); try std.testing.expectError( error.ConflictRootNotFound, store.connection.commit(&store.history), ); try std.testing.expect(version.same(baseline, (try store.connection.checkout()).head)); try std.testing.expect(version.same(baseline, (try store.history.ref("main")).?.target)); try std.testing.expect(!store.history.hasDatabaseRoot(invalid_root));}test "connection commit rejects catalog identity drift" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var store = TestingConnection{}; try store.init(std.testing.allocator, tmp.dir, "catalog-drift.db", "catalog-drift.wal", "catalog-drift.history"); defer store.deinit(); try createItems(&store.connection); try store.connection.stage(); const baseline = try store.connection.commit(&store.history); try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'staged')"); try store.connection.stage(); const staged = (try store.connection.workingRoot()); _ = try store.connection.catalog.createRelation(std.testing.allocator, .{ .name = "untracked", .columns = &.{.{ .name = "value" }}, }, .{ .durability = .buffered }); try std.testing.expectError(error.InvalidHistory, store.connection.commit(&store.history)); try std.testing.expect(version.same(baseline, (try store.connection.checkout()).head)); try std.testing.expect(version.same(baseline, (try store.history.ref("main")).?.target)); try std.testing.expect(version.same(staged, (try store.connection.workingRoot()))); try std.testing.expect(!store.history.hasDatabaseRoot(staged));}test "connection fast forward missing database root is failure atomic" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var store = TestingConnection{}; try store.init( std.testing.allocator, tmp.dir, "atomic-missing.db", "atomic-missing.wal", "atomic-missing.history", ); defer store.deinit(); try createItems(&store.connection); try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'before')"); try store.connection.stage(); const baseline_head = try store.connection.commit(&store.history); const baseline_root = (try store.connection.workingRoot()); const missing_root = version.emptyHash("sql.checkout.missing.database"); const parents = [_]version.Hash{baseline_head}; const target = version.Commit.init(missing_root, &parents); try store.history.putCommit(target); try std.testing.expectError( error.DatabaseRootNotFound, store.connection.fastForwardBranch(std.testing.allocator, &store.history, target.hash), ); try expectAtomicConnectionBaseline(&store, baseline_head, baseline_root, null);}test "connection fast forward rejects a changed working set" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var store = TestingConnection{}; try store.init( std.testing.allocator, tmp.dir, "fast-forward-dirty.db", "fast-forward-dirty.wal", "fast-forward-dirty.history", ); defer store.deinit(); try createItems(&store.connection); try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'baseline')"); try store.connection.stage(); const baseline = try store.connection.commit(&store.history); _ = try store.connection.createBranch(&store.history, "target"); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "target"); try executeStatement(&store.connection, "UPDATE items SET name = 'target' WHERE rowid = 1"); try store.connection.stage(); const target = try store.connection.commit(&store.history); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main"); try executeStatement(&store.connection, "UPDATE items SET name = 'dirty' WHERE rowid = 1"); const dirty = (try store.connection.workingRoot()); try std.testing.expectError( error.WorkingSetChanged, store.connection.fastForwardBranch(std.testing.allocator, &store.history, target), ); try std.testing.expect(version.same(baseline, (try store.connection.checkout()).head)); try std.testing.expect(version.same(baseline, (try store.history.ref("main")).?.target)); try std.testing.expect(version.same(dirty, (try store.connection.workingRoot()))); var result = try store.connection.execute( std.testing.allocator, "SELECT name FROM items WHERE rowid = 1", .{ .durability = .buffered }, ); defer result.deinit(std.testing.allocator); const view = try row.View.init(result.nextRow().?); try std.testing.expectEqualStrings("dirty", (try view.column(0)).text);}test "connection fast forward rejects an unsynced semantic clean state" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var store = TestingConnection{}; try store.init( std.testing.allocator, tmp.dir, "fast-forward-unsynced.db", "fast-forward-unsynced.wal", "fast-forward-unsynced.history", ); defer store.deinit(); try createItems(&store.connection); try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'baseline')"); try store.connection.stage(); const baseline = try store.connection.commit(&store.history); _ = try store.connection.createBranch(&store.history, "target"); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "target"); try executeStatement(&store.connection, "UPDATE items SET name = 'target' WHERE rowid = 1"); try store.connection.stage(); const target = try store.connection.commit(&store.history); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main"); try executeStatement(&store.connection, "UPDATE items SET name = 'temporary' WHERE rowid = 1"); try executeStatement(&store.connection, "UPDATE items SET name = 'baseline' WHERE rowid = 1"); try std.testing.expect(version.same(baseline, (try store.connection.checkout()).head)); try std.testing.expect(!(try store.connection.checkout()).working.dirty()); try std.testing.expect(!store.database.walSynced()); try std.testing.expectError( error.WorkingSetChanged, store.connection.fastForwardBranch(std.testing.allocator, &store.history, target), ); try std.testing.expect(version.same(baseline, (try store.history.ref("main")).?.target));}test "connection open repairs pending fast forward to baseline" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var store = TestingConnection{}; try store.init( std.testing.allocator, tmp.dir, "recover-prepare.db", "recover-prepare.wal", "recover-prepare.history", ); defer store.deinit(); try createItems(&store.connection); try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'baseline')"); try store.connection.stage(); const baseline = try store.connection.commit(&store.history); _ = try store.connection.createBranch(&store.history, "target"); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "target"); try executeStatement(&store.connection, "UPDATE items SET name = 'target' WHERE rowid = 1"); try store.connection.stage(); const target = try store.connection.commit(&store.history); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main"); const target_commit = try store.history.commitValue(target); var target_value = try store.history.databaseValue( std.testing.allocator, target_commit.root, ); defer target_value.deinit(); _ = try store.history.beginFastForward("main", baseline, target); try store.connection.materializeDatabaseValue( std.testing.allocator, &target_value, .{ .durability = .synced }, ); store.connection.deinit(); store.connection = try Connection.open( std.testing.allocator, &store.database, &store.history, .{}, ); try std.testing.expect(store.history.fastForwardRecovery() == null); try std.testing.expect(version.same(baseline, (try store.history.ref("main")).?.target)); try std.testing.expect(version.same(baseline, (try store.connection.checkout()).head)); var result = try store.connection.execute( std.testing.allocator, "SELECT name FROM items WHERE rowid = 1", .{ .durability = .buffered }, ); defer result.deinit(std.testing.allocator); const view = try row.View.init(result.nextRow().?); try std.testing.expectEqualStrings("baseline", (try view.column(0)).text);}test "connection open repairs committed fast forward to target" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var store = TestingConnection{}; try store.init( std.testing.allocator, tmp.dir, "recover-decision.db", "recover-decision.wal", "recover-decision.history", ); defer store.deinit(); try createItems(&store.connection); try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'baseline')"); try store.connection.stage(); const baseline = try store.connection.commit(&store.history); _ = try store.connection.createBranch(&store.history, "target"); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "target"); try executeStatement(&store.connection, "UPDATE items SET name = 'target' WHERE rowid = 1"); try store.connection.stage(); const target = try store.connection.commit(&store.history); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main"); var update = try store.history.beginFastForward("main", baseline, target); try update.commit(); store.connection.deinit(); store.connection = try Connection.open( std.testing.allocator, &store.database, &store.history, .{}, ); try std.testing.expect(store.history.fastForwardRecovery() == null); try std.testing.expect(version.same(target, (try store.history.ref("main")).?.target)); try std.testing.expect(version.same(target, (try store.connection.checkout()).head)); var result = try store.connection.execute( std.testing.allocator, "SELECT name FROM items WHERE rowid = 1", .{ .durability = .buffered }, ); defer result.deinit(std.testing.allocator); const view = try row.View.init(result.nextRow().?); try std.testing.expectEqualStrings("target", (try view.column(0)).text);}test "connection checkout row root mismatch is failure atomic" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var store = TestingConnection{}; try store.init( std.testing.allocator, tmp.dir, "atomic-mismatch.db", "atomic-mismatch.wal", "atomic-mismatch.history", ); defer store.deinit(); try createItems(&store.connection); try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'before')"); try executeStatement(&store.connection, "CREATE TABLE dropme (name)"); try executeStatement(&store.connection, "INSERT INTO dropme (rowid, name) VALUES (7, 'kept')"); try executeStatement(&store.connection, "ANALYZE dropme"); try store.connection.stage(); const baseline_head = try store.connection.commit(&store.history); const baseline_root = (try store.connection.workingRoot()); var baseline_dropme = try store.connection.relationView(std.testing.allocator, "dropme"); const baseline_stats = baseline_dropme.root.stats.hash; baseline_dropme.deinit(); try executeStatement(&store.connection, "DROP TABLE dropme"); try executeStatement(&store.connection, "UPDATE items SET name = 'target' WHERE rowid = 1"); var target_value = try store.connection.materializedWorkingValue(std.testing.allocator); defer target_value.deinit(); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main"); try executeStatement(&store.connection, "DROP TABLE dropme"); try executeStatement(&store.connection, "UPDATE items SET name = 'wrong' WHERE rowid = 1"); var wrong_value = try store.connection.materializedWorkingValue(std.testing.allocator); defer wrong_value.deinit(); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main"); const target_relation = target_value.findRelation("items") orelse return error.TestUnexpectedResult; const wrong_relation = wrong_value.findRelation("items") orelse return error.TestUnexpectedResult; try store.history.putRelationRoot(target_relation.root); try store.history.putRelationRows(target_relation.root.hash, wrong_relation.rows); try store.history.putDatabaseRoot(target_value.root); const parents = [_]version.Hash{baseline_head}; const target_commit = version.Commit.init(target_value.root.hash, &parents); try store.history.putCommit(target_commit); _ = try store.history.createBranch("bad", target_commit.hash); try std.testing.expectError( error.InvalidHistory, store.connection.checkoutBranch(std.testing.allocator, &store.history, "bad"), ); try expectAtomicConnectionBaseline(&store, baseline_head, baseline_root, baseline_stats); try std.testing.expect(version.same(target_commit.hash, (try store.history.ref("bad")).?.target));}test "connection fast forward allocation failures are atomic" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var store = TestingConnection{}; try store.init( std.testing.allocator, tmp.dir, "atomic-oom.db", "atomic-oom.wal", "atomic-oom.history", ); defer store.deinit(); try createItems(&store.connection); try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'before')"); try executeStatement(&store.connection, "CREATE TABLE dropme (name)"); try executeStatement(&store.connection, "INSERT INTO dropme (rowid, name) VALUES (7, 'kept')"); try executeStatement(&store.connection, "ANALYZE dropme"); try store.connection.stage(); const baseline_head = try store.connection.commit(&store.history); const baseline_root = (try store.connection.workingRoot()); var baseline_dropme = try store.connection.relationView(std.testing.allocator, "dropme"); const baseline_stats = baseline_dropme.root.stats.hash; baseline_dropme.deinit(); _ = try store.connection.createBranch(&store.history, "target"); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "target"); try executeStatement(&store.connection, "DROP TABLE dropme"); try executeStatement(&store.connection, "UPDATE items SET name = 'after' WHERE rowid = 1"); try executeStatement(&store.connection, "CREATE TABLE alpha (name)"); try executeStatement(&store.connection, "INSERT INTO alpha (rowid, name) VALUES (2, 'new')"); try store.connection.stage(); const target_head = try store.connection.commit(&store.history); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main"); var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); const warm_start = failing.alloc_index; try store.connection.fastForwardBranch(failing.allocator(), &store.history, target_head); const operation_allocations = failing.alloc_index - warm_start; try std.testing.expect(operation_allocations > 0); try store.history.putRef(.{ .name = "main", .target = baseline_head }); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main"); const offsets = [_]usize{ 0, operation_allocations / 2, operation_allocations - 1 }; for (offsets) |offset| { failing.fail_index = failing.alloc_index + offset; failing.resize_fail_index = std.math.maxInt(usize); try std.testing.expectError( error.OutOfMemory, store.connection.fastForwardBranch(failing.allocator(), &store.history, target_head), ); try std.testing.expect(failing.has_induced_failure); failing.fail_index = std.math.maxInt(usize); failing.resize_fail_index = std.math.maxInt(usize); failing.has_induced_failure = false; try expectAtomicConnectionBaseline(&store, baseline_head, baseline_root, baseline_stats); }}const TestingConnection = struct { database: file.Database = undefined, history: history_mod.History = undefined, connection: Connection = undefined, fn init(self: *TestingConnection, allocator: Allocator, dir: std.Io.Dir, database_path: []const u8, wal_path: []const u8, history_path: []const u8) !void { self.database = try file.Database.openForTesting(allocator, dir, .{ .paths = .{ .database = database_path, .wal = wal_path }, .header = testingHeader(), }); errdefer self.database.deinit(); try self.database.reserve(.{ .wal_frames = 960 }); self.history = try history_mod.History.open(allocator, dir, .{ .path = history_path, .recovery = .reject }); errdefer self.history.deinit(); self.connection = try Connection.create(allocator, &self.database, &self.history, .{}); } fn deinit(self: *TestingConnection) void { self.connection.deinit(); self.history.deinit(); self.database.deinit(); self.* = undefined; }};const TestingConflict = struct { commit: version.Hash, root: version.Hash, artifact: version.Hash, database: version.Hash,};fn commitTestingConflict(store: *TestingConnection) !TestingConflict { try createItems(&store.connection); try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'base')"); try store.connection.stage(); _ = try store.connection.commit(&store.history); _ = try store.connection.createBranch(&store.history, "side"); _ = try store.connection.createBranch(&store.history, "behind"); try executeStatement(&store.connection, "UPDATE items SET name = 'ours' WHERE rowid = 1"); try store.connection.stage(); _ = try store.connection.commit(&store.history); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "side"); try executeStatement(&store.connection, "UPDATE items SET name = 'theirs' WHERE rowid = 1"); try store.connection.stage(); const side_commit = try store.connection.commit(&store.history); try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main"); var merged = try store.connection.mergeBranch(std.testing.allocator, &store.history, "side", .{}); defer merged.deinit(); try std.testing.expect(merged.hasConflicts()); try std.testing.expectEqual(@as(usize, 1), merged.conflict_root.count); const root = merged.conflict_root.hash; const artifact = merged.discovered[0].artifact.hash; try store.connection.stage(); const commit = try store.connection.mergeCommit(&store.history, side_commit); return .{ .commit = commit, .root = root, .artifact = artifact, .database = (try store.connection.workingRoot()) };}fn expectTestingConflict(store: *TestingConnection, conflict: TestingConflict) !void { try std.testing.expect(version.same(conflict.root, store.connection.session.workingRoot().conflicts)); var artifacts = try store.connection.conflictArtifacts(std.testing.allocator, &store.history); defer artifacts.deinit(); try std.testing.expectEqual(@as(usize, 1), artifacts.artifacts.len); try std.testing.expect(version.same(conflict.artifact, artifacts.artifacts[0].hash));}fn createItems(connection: *Connection) !void { try executeStatement(connection, "CREATE TABLE items (name)");}fn executeStatement(connection: *Connection, source: []const u8) !void { var result = try connection.execute(std.testing.allocator, source, .{ .durability = .buffered }); defer result.deinit(std.testing.allocator);}fn expectLiveDatabaseRoot(connection: *Connection, target: *const version.DatabaseRoot) !void { var live = try version.databaseRoot( std.testing.allocator, &connection.catalog, target.conflicts, ); defer live.deinit(); try std.testing.expect(version.same(live.hash, target.hash));}fn expectAtomicConnectionBaseline( store: *TestingConnection, head: version.Hash, root: version.Hash, dropme_stats: ?version.Hash,) !void { const checkout_value = (try store.connection.checkout()); try std.testing.expectEqualStrings("main", checkout_value.name); try std.testing.expect(version.same(head, checkout_value.head)); try std.testing.expect(version.same(head, (try store.history.ref("main")).?.target)); try std.testing.expect(version.same(root, checkout_value.working.base)); try std.testing.expect(version.same(root, checkout_value.working.working)); try std.testing.expect(version.same(root, checkout_value.working.staged)); try std.testing.expect(version.same(root, (try store.connection.workingRoot()))); var item = try store.connection.execute( std.testing.allocator, "SELECT name FROM items WHERE rowid = 1", .{ .durability = .buffered }, ); defer item.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), item.rowCount()); const item_view = try row.View.init(item.nextRow().?); try std.testing.expectEqualStrings("before", (try item_view.column(0)).text); if (dropme_stats) |stats_hash| { var kept = try store.connection.execute( std.testing.allocator, "SELECT name FROM dropme WHERE rowid = 7", .{ .durability = .buffered }, ); defer kept.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), kept.rowCount()); const kept_view = try row.View.init(kept.nextRow().?); try std.testing.expectEqualStrings("kept", (try kept_view.column(0)).text); var stats = (try store.connection.catalog.relationStats(std.testing.allocator, "dropme")).?; defer stats.deinit(); try std.testing.expectEqual(@as(usize, 1), stats.table.entries); var relation_view = try store.connection.relationView(std.testing.allocator, "dropme"); defer relation_view.deinit(); try std.testing.expect(version.same(stats_hash, relation_view.root.stats.hash)); } var live_root = try version.databaseRootMaintained( std.testing.allocator, &store.connection.catalog, store.connection.session.workingRoot().conflicts, ); defer live_root.deinit(); try std.testing.expect(version.same(root, live_root.hash));}fn testEntryHash(root: *const version.DatabaseRoot, name: []const u8) version.Hash { return testFindEntry(root, name) orelse unreachable;}fn testFindEntry(root: *const version.DatabaseRoot, name: []const u8) ?version.Hash { for (root.entries) |entry| { if (std.mem.eql(u8, entry.name, name)) return entry.hash; } return null;}fn hasConflictHash(entries: []const version.ConflictEntry, hash: version.Hash) bool { for (entries) |entry| { if (version.same(entry.hash, hash)) return true; } return false;}fn hasConflictHashValue(hashes: []const version.Hash, hash: version.Hash) bool { for (hashes) |candidate| { if (version.same(candidate, hash)) return true; } return false;}fn testingHeader() wal.Header { return .{ .sequence = 3901, .salt = .{ .first = 0x1357_3901, .second = 0x2468_3901 }, };}Source: lib/sql/src/root.zig:26
zig
pub const connection = @import("connection.zig");Audit
| Definitions | 2 |
|---|---|
| Public names | 2 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |