lib/sql/src/connection.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const simd = @import("simd");
   3 const branch_mod = @import("branch.zig");
   4 const catalog_mod = @import("catalog.zig");
   5 const diff_mod = @import("diff.zig");
   6 const file = @import("file.zig");
   7 const history_mod = @import("history/root.zig");
   8 const merge_mod = @import("merge.zig");
   9 const plan_mod = @import("plan.zig");
  10 const relation_mod = @import("relation.zig");
  11 const session_mod = @import("session/root.zig");
  12 const statement_mod = @import("statement/root.zig");
  13 const row = @import("row.zig");
  14 const tree = @import("tree.zig");
  15 const version = @import("version.zig");
  16 const wal = @import("wal.zig");
  17 
  18 const Bytes = simd.ScalableTag(u8);
  19 
  20 const Allocator = std.mem.Allocator;
  21 
  22 const ConnectionError = error{
  23     ConflictNotFound,
  24     NoMergeBase,
  25     UnsupportedCheckoutRoot,
  26     WorkingSetChanged,
  27 };
  28 
  29 pub const Error =
  30     statement_mod.Error ||
  31     history_mod.Error ||
  32     diff_mod.Error ||
  33     merge_mod.Error ||
  34     version.Error ||
  35     ConnectionError;
  36 
  37 pub const Options = struct {
  38     branch: []const u8 = "main",
  39     catalog: catalog_mod.Options = .{},
  40 };
  41 
  42 pub const ExecuteOptions = struct {
  43     durability: file.CommitDurability = .synced,
  44     validate_indexes: bool = false,
  45     write: ?*session_mod.DatabaseWrite = null,
  46 
  47     fn statement(self: ExecuteOptions, session: *session_mod.DatabaseSession) statement_mod.ExecuteOptions {
  48         return .{
  49             .durability = self.durability,
  50             .validate_indexes = self.validate_indexes,
  51             .session = session,
  52             .write = self.write,
  53         };
  54     }
  55 };
  56 
  57 pub const WriteOptions = struct {
  58     limits: session_mod.DatabaseWrite.Limits,
  59     durability: file.CommitDurability = .synced,
  60     validate_indexes: bool = false,
  61 
  62     fn commit(self: WriteOptions) file.CommitOptions {
  63         return .{
  64             .durability = self.durability,
  65             .validate_indexes = self.validate_indexes,
  66         };
  67     }
  68 };
  69 
  70 pub const MergeOptions = struct {
  71     commit: file.CommitOptions = .{ .durability = .buffered },
  72 };
  73 
  74 pub const RelationView = struct {
  75     allocator: Allocator,
  76     root: version.RelationRoot,
  77     handle: ?catalog_mod.RelationHandle = null,
  78     rows: []version.RelationRow = &.{},
  79 
  80     pub fn snapshot(self: *const RelationView) diff_mod.RelationSnapshot {
  81         return .{
  82             .root = &self.root,
  83             .rows = if (self.handle) |*handle| .{ .live = handle } else .{ .materialized = self.rows },
  84         };
  85     }
  86 
  87     pub fn deinit(self: *RelationView) void {
  88         self.root.deinit();
  89         if (self.handle) |*handle| handle.deinit();
  90         version.freeRelationRows(self.allocator, self.rows);
  91         self.* = undefined;
  92     }
  93 };
  94 
  95 pub const DatabaseView = struct {
  96     allocator: Allocator,
  97     relations: []RelationView,
  98     snapshots: []diff_mod.RelationSnapshot,
  99     conflicts: history_mod.ConflictArtifacts,
 100 
 101     pub fn snapshot(self: *const DatabaseView) merge_mod.DatabaseSnapshot {
 102         return .{
 103             .relations = self.snapshots,
 104             .conflicts = .{
 105                 .root = self.conflicts.root,
 106                 .artifacts = self.conflicts.artifacts,
 107             },
 108         };
 109     }
 110 
 111     pub fn deinit(self: *DatabaseView) void {
 112         for (self.relations) |*relation_view| relation_view.deinit();
 113         if (self.relations.len != 0) self.allocator.free(self.relations);
 114         if (self.snapshots.len != 0) self.allocator.free(self.snapshots);
 115         self.conflicts.deinit();
 116         self.* = undefined;
 117     }
 118 };
 119 
 120 pub const Connection = struct {
 121     catalog: catalog_mod.Catalog,
 122     session: session_mod.DatabaseSession,
 123     recovery_required: bool = false,
 124 
 125     pub fn init(catalog: catalog_mod.Catalog, session: session_mod.DatabaseSession) Connection {
 126         return .{
 127             .catalog = catalog,
 128             .session = session,
 129         };
 130     }
 131 
 132     pub fn create(allocator: Allocator, database: *file.Database, history: *history_mod.History, options: Options) Error!Connection {
 133         var catalog = try catalog_mod.Catalog.open(database, options.catalog);
 134         var value = try version.databaseValue(
 135             allocator,
 136             &catalog,
 137             version.ConflictRoot.empty().hash,
 138         );
 139         var value_live = true;
 140         errdefer if (value_live) value.deinit();
 141         const root_commit = version.Commit.init(value.root.hash, &.{});
 142         try database.syncWal();
 143         try publishDatabaseValue(history, &value);
 144         try history.putCommit(root_commit);
 145         _ = try history.createBranch(options.branch, root_commit.hash);
 146         const checkout_value = try history.checkoutBranch(options.branch);
 147         var root = value.intoRoot();
 148         value_live = false;
 149         const session = session_mod.DatabaseSession.initWithRoot(allocator, checkout_value, &root);
 150         return Connection.init(catalog, session);
 151     }
 152 
 153     pub fn openLocal(allocator: Allocator, database: *file.Database, options: Options) Error!Connection {
 154         const catalog = try catalog_mod.Catalog.open(database, options.catalog);
 155         var root = try version.databaseRootMaintained(
 156             allocator,
 157             &catalog,
 158             version.ConflictRoot.empty().hash,
 159         );
 160         errdefer root.deinit();
 161         const root_commit = version.Commit.init(root.hash, &.{});
 162         const checkout_value = branch_mod.checkout(.{
 163             .name = options.branch,
 164             .target = root_commit.hash,
 165         }, root.hash);
 166         return Connection.init(
 167             catalog,
 168             session_mod.DatabaseSession.initWithRoot(allocator, checkout_value, &root),
 169         );
 170     }
 171 
 172     pub fn open(allocator: Allocator, database: *file.Database, history: *history_mod.History, options: Options) Error!Connection {
 173         try recoverFastForward(allocator, database, history, options.catalog);
 174         const catalog = try catalog_mod.Catalog.open(database, options.catalog);
 175         const checkout_value = try history.checkoutBranch(options.branch);
 176         var target = try history.databaseRoot(allocator, checkout_value.working.working);
 177         defer target.deinit();
 178         try history.validateConflictRoot(target.conflicts);
 179         var root = try version.databaseRootMaintained(allocator, &catalog, target.conflicts);
 180         errdefer root.deinit();
 181         const live_checkout = checkout_value.withWorking(root.hash);
 182         return Connection.init(catalog, session_mod.DatabaseSession.initWithRoot(allocator, live_checkout, &root));
 183     }
 184 
 185     pub fn adoptRebuiltHistory(self: *Connection, history: *history_mod.History, branch_name: []const u8) Error!void {
 186         try self.ensureUsable();
 187         std.debug.assert(history.len() == 0);
 188         const allocator = self.session.allocator;
 189         var value = try version.databaseValue(
 190             allocator,
 191             &self.catalog,
 192             version.ConflictRoot.empty().hash,
 193         );
 194         var value_live = true;
 195         errdefer if (value_live) value.deinit();
 196         const root_commit = version.Commit.init(value.root.hash, &.{});
 197         try self.catalog.database.syncWal();
 198         try publishDatabaseValue(history, &value);
 199         try history.putCommit(root_commit);
 200         _ = try history.createBranch(branch_name, root_commit.hash);
 201         const checkout_value = try history.checkoutBranch(branch_name);
 202         var root = value.intoRoot();
 203         value_live = false;
 204         self.session.reinitWithRoot(checkout_value, &root);
 205     }
 206 
 207     pub fn deinit(self: *Connection) void {
 208         self.session.deinit();
 209         self.* = undefined;
 210     }
 211 
 212     pub fn lastRowId(self: *const Connection, allocator: Allocator, table_name: []const u8) Error!?i64 {
 213         try self.ensureUsable();
 214         var handle = try self.catalog.openRelation(allocator, table_name);
 215         defer handle.deinit();
 216         return try handle.relation.lastRowId();
 217     }
 218 
 219     pub fn prepare(self: *const Connection, allocator: Allocator, source: []const u8) statement_mod.Error!statement_mod.Prepared {
 220         try self.ensureUsable();
 221         return try statement_mod.prepare(&self.catalog, allocator, source);
 222     }
 223 
 224     pub fn execute(self: *Connection, allocator: Allocator, source: []const u8, options: ExecuteOptions) statement_mod.Error!statement_mod.Result {
 225         var prepared = try self.prepare(allocator, source);
 226         defer prepared.deinit();
 227         return try self.executePrepared(&prepared, allocator, options);
 228     }
 229 
 230     pub fn executePrepared(self: *Connection, prepared: *statement_mod.Prepared, allocator: Allocator, options: ExecuteOptions) statement_mod.Error!statement_mod.Result {
 231         try self.ensureUsable();
 232         return try prepared.execute(allocator, options.statement(&self.session));
 233     }
 234 
 235     /// Opens a cursor over the rows `prepared` selects in `target`.
 236     pub fn openCursor(
 237         self: *Connection,
 238         target: *statement_mod.Cursor,
 239         prepared: *statement_mod.Prepared,
 240         allocator: Allocator,
 241     ) statement_mod.Error!void {
 242         try self.ensureUsable();
 243         try prepared.openCursor(target, allocator);
 244     }
 245 
 246     pub fn beginWrite(
 247         self: *Connection,
 248         workspace: *session_mod.DatabaseWrite.Workspace,
 249         flush_allocator: Allocator,
 250         options: WriteOptions,
 251     ) session_mod.DatabaseError!session_mod.DatabaseWrite {
 252         try self.ensureUsable();
 253         return try self.session.beginWrite(
 254             workspace,
 255             flush_allocator,
 256             options.limits,
 257             options.commit(),
 258         );
 259     }
 260 
 261     pub fn stage(self: *Connection) Error!void {
 262         try self.ensureUsable();
 263         self.session.stage();
 264     }
 265 
 266     pub fn commit(self: *Connection, history: *history_mod.History) Error!version.Hash {
 267         try self.ensureUsable();
 268         if (!self.session.checkout.working.hasStaged()) return error.NoStagedRoot;
 269         try self.catalog.database.syncWal();
 270         try self.publishStagedValue(history, self.session.checkout.working.staged);
 271         return try self.session.commit(history);
 272     }
 273 
 274     pub fn commitLocal(self: *Connection) Error!version.Hash {
 275         try self.ensureUsable();
 276         if (!self.session.checkout.working.hasStaged()) return error.NoStagedRoot;
 277         const root = self.session.checkout.working.staged;
 278         const parents = [_]version.Hash{self.session.checkout.head};
 279         const commit_value = version.Commit.init(root, &parents);
 280         try self.session.advance(commit_value.hash, root);
 281         return commit_value.hash;
 282     }
 283 
 284     pub fn mergeCommit(self: *Connection, history: *history_mod.History, theirs: version.Hash) Error!version.Hash {
 285         try self.ensureUsable();
 286         const root = self.session.checkout.working.working;
 287         try self.catalog.database.syncWal();
 288         try self.publishStagedValue(history, root);
 289         const commit_hash = try history.mergeCommitBranch(self.session.checkout.name, root, theirs);
 290         try self.session.advance(commit_hash, root);
 291         return commit_hash;
 292     }
 293 
 294     pub fn createBranch(self: *const Connection, history: *history_mod.History, name: []const u8) history_mod.Error!version.Ref {
 295         try self.ensureUsable();
 296         return try history.createBranch(name, self.session.checkout.head);
 297     }
 298 
 299     pub fn checkoutBranch(self: *Connection, allocator: Allocator, history: *const history_mod.History, name: []const u8) Error!void {
 300         try self.ensureUsable();
 301         const checkout_value = try history.checkoutBranch(name);
 302         var value = try history.databaseValue(allocator, checkout_value.working.working);
 303         var value_live = true;
 304         errdefer if (value_live) value.deinit();
 305         try history.validateConflictRoot(value.root.conflicts);
 306         try self.materializeDatabaseValue(allocator, &value, .{ .durability = .synced });
 307         var root = value.intoRoot();
 308         value_live = false;
 309         self.session.reinitWithRoot(checkout_value, &root);
 310     }
 311 
 312     pub fn fastForwardBranch(self: *Connection, allocator: Allocator, history: *history_mod.History, target: version.Hash) Error!void {
 313         try self.ensureUsable();
 314         var next_ref = (try history.ref(self.session.checkout.name)) orelse return error.RefNotFound;
 315         const baseline = try self.validateFastForwardBaseline(allocator, history, next_ref);
 316         const entries = try history.commitEntries(allocator);
 317         defer allocator.free(entries);
 318         try branch_mod.fastForwardRef(allocator, entries, &next_ref, target);
 319         const commit_value = try history.commitValue(target);
 320         var value = try history.databaseValue(allocator, commit_value.root);
 321         var value_live = true;
 322         errdefer if (value_live) value.deinit();
 323         try history.validateConflictRoot(value.root.conflicts);
 324         var plan = try DatabaseMaterialization.init(self, allocator, &value);
 325         defer plan.deinit();
 326         try self.coordinateFastForward(
 327             allocator,
 328             history,
 329             next_ref,
 330             baseline,
 331             &value,
 332             &value_live,
 333             &plan,
 334         );
 335     }
 336 
 337     fn validateFastForwardBaseline(
 338         self: *Connection,
 339         allocator: Allocator,
 340         history: *const history_mod.History,
 341         current_ref: version.Ref,
 342     ) Error!version.Hash {
 343         if (!self.catalog.database.walSynced()) return error.WorkingSetChanged;
 344         if (!version.same(current_ref.target, self.session.checkout.head)) return error.RefChanged;
 345         const working = self.session.checkout.working;
 346         if (!version.same(working.base, working.working) or
 347             !version.same(working.base, working.staged) or
 348             !version.same(working.base, self.session.workingRoot().hash) or
 349             self.session.pendingRelations() != 0)
 350         {
 351             return error.WorkingSetChanged;
 352         }
 353         const current_commit = try history.commitValue(self.session.checkout.head);
 354         if (!version.same(current_commit.root, working.base)) return error.WorkingSetChanged;
 355         var live_root = try version.databaseRootMaintained(
 356             allocator,
 357             &self.catalog,
 358             self.session.workingRoot().conflicts,
 359         );
 360         defer live_root.deinit();
 361         if (!version.same(live_root.hash, working.base)) return error.WorkingSetChanged;
 362         return working.base;
 363     }
 364 
 365     fn coordinateFastForward(
 366         self: *Connection,
 367         allocator: Allocator,
 368         history: *history_mod.History,
 369         next_ref: version.Ref,
 370         baseline: version.Hash,
 371         value: *version.DatabaseValue,
 372         value_live: *bool,
 373         plan: *DatabaseMaterialization,
 374     ) Error!void {
 375         try self.catalog.database.syncWal();
 376         const savepoint = try self.catalog.database.savepoint();
 377         try self.catalog.database.beginCoordinator();
 378         var coordinator_active = true;
 379         errdefer if (coordinator_active and !self.catalog.database.requiresRecovery()) {
 380             self.catalog.database.endCoordinator();
 381         };
 382         var update = history.beginFastForward(
 383             next_ref.name,
 384             self.session.checkout.head,
 385             next_ref.target,
 386         ) catch |err| {
 387             if (err == error.RecoveryRequired) {
 388                 self.poisonFastForward(history);
 389                 return error.RecoveryRequired;
 390             }
 391             self.catalog.database.endCoordinator();
 392             coordinator_active = false;
 393             return err;
 394         };
 395         plan.apply(self, .{ .durability = .synced }) catch |err| {
 396             try self.restoreFastForward(history, &update, savepoint, baseline);
 397             coordinator_active = false;
 398             return err;
 399         };
 400         self.verifyMaterializedDatabaseRoot(allocator, &value.root) catch |err| {
 401             try self.restoreFastForward(history, &update, savepoint, baseline);
 402             coordinator_active = false;
 403             return err;
 404         };
 405         update.commit() catch |err| {
 406             if (err == error.RecoveryRequired) {
 407                 self.poisonFastForward(history);
 408                 return error.RecoveryRequired;
 409             }
 410             try self.restoreFastForward(history, &update, savepoint, baseline);
 411             coordinator_active = false;
 412             return err;
 413         };
 414         const checkout_value = branch_mod.checkout(next_ref, value.root.hash);
 415         var root = value.intoRoot();
 416         value_live.* = false;
 417         self.session.reinitWithRoot(checkout_value, &root);
 418         try self.completeFastForward(history, &update);
 419         self.catalog.database.endCoordinator();
 420         coordinator_active = false;
 421     }
 422 
 423     fn verifyMaterializedDatabaseRoot(
 424         self: *Connection,
 425         allocator: Allocator,
 426         target: *const version.DatabaseRoot,
 427     ) Error!void {
 428         var names = try self.catalog.relationNames(allocator);
 429         defer names.deinit();
 430         if (names.names.len != target.entries.len) return error.InvalidHistory;
 431         for (names.names) |name| {
 432             const entry = relationEntry(target.entries, name) orelse return error.InvalidHistory;
 433             var state = try self.catalog.readRelation(allocator, name);
 434             defer state.deinit();
 435             const live = try version.relationKey(name, &state.handle, state.relationStats());
 436             if (!version.same(live.hash, entry.hash)) return error.InvalidHistory;
 437         }
 438         var verified = try version.databaseRootMaintained(
 439             allocator,
 440             &self.catalog,
 441             target.conflicts,
 442         );
 443         defer verified.deinit();
 444         if (!version.same(verified.hash, target.hash)) return error.InvalidHistory;
 445     }
 446 
 447     fn restoreFastForward(
 448         self: *Connection,
 449         history: *history_mod.History,
 450         update: *history_mod.FastForwardUpdate,
 451         savepoint: file.Savepoint,
 452         baseline: version.Hash,
 453     ) Error!void {
 454         self.rollbackFastForward(update, savepoint, baseline) catch {
 455             self.poisonFastForward(history);
 456             return error.RecoveryRequired;
 457         };
 458     }
 459 
 460     fn completeFastForward(
 461         self: *Connection,
 462         history: *history_mod.History,
 463         update: *history_mod.FastForwardUpdate,
 464     ) Error!void {
 465         update.complete() catch |err| {
 466             if (err != error.RecoveryRequired) update.complete() catch {
 467                 self.poisonFastForward(history);
 468                 return error.RecoveryRequired;
 469             } else {
 470                 self.poisonFastForward(history);
 471                 return error.RecoveryRequired;
 472             }
 473         };
 474     }
 475 
 476     fn rollbackFastForward(
 477         self: *Connection,
 478         update: *history_mod.FastForwardUpdate,
 479         savepoint: file.Savepoint,
 480         baseline: version.Hash,
 481     ) Error!void {
 482         try self.catalog.database.restore(savepoint);
 483         try self.catalog.database.syncWal();
 484         var restored = try version.databaseRootMaintained(
 485             self.session.allocator,
 486             &self.catalog,
 487             self.session.workingRoot().conflicts,
 488         );
 489         defer restored.deinit();
 490         if (!version.same(restored.hash, baseline)) return error.RecoveryRequired;
 491         try update.abort();
 492         try update.complete();
 493         self.catalog.database.endCoordinator();
 494     }
 495 
 496     fn poisonFastForward(self: *Connection, history: *history_mod.History) void {
 497         self.recovery_required = true;
 498         self.catalog.database.poison();
 499         history.poison();
 500     }
 501 
 502     fn ensureUsable(self: *const Connection) error{RecoveryRequired}!void {
 503         if (self.recovery_required or self.catalog.database.requiresRecovery()) {
 504             return error.RecoveryRequired;
 505         }
 506     }
 507 
 508     pub fn mergeBase(self: *const Connection, allocator: Allocator, history: *const history_mod.History, theirs: []const u8) history_mod.Error!?version.Hash {
 509         try self.ensureUsable();
 510         const theirs_ref = (try history.ref(theirs)) orelse return error.RefNotFound;
 511         const entries = try history.commitEntries(allocator);
 512         defer allocator.free(entries);
 513         return try branch_mod.mergeBase(allocator, entries, self.session.checkout.head, theirs_ref.target);
 514     }
 515 
 516     pub fn relationView(self: *const Connection, allocator: Allocator, name: []const u8) Error!RelationView {
 517         try self.ensureUsable();
 518         var state = try self.catalog.readRelation(allocator, name);
 519         errdefer state.handle.deinit();
 520         defer if (state.stats) |*relation_stats| relation_stats.deinit();
 521         var root = try version.relationRoot(
 522             allocator,
 523             name,
 524             state.schema,
 525             &state.handle,
 526             state.relationStats(),
 527         );
 528         errdefer root.deinit();
 529         return .{
 530             .allocator = allocator,
 531             .root = root,
 532             .handle = state.handle,
 533         };
 534     }
 535 
 536     pub fn databaseView(self: *const Connection, allocator: Allocator, history: *const history_mod.History, names: []const []const u8) Error!DatabaseView {
 537         try self.ensureUsable();
 538         var conflicts = try history.conflictArtifacts(allocator, self.session.workingRoot().conflicts);
 539         errdefer conflicts.deinit();
 540 
 541         const relations = try allocator.alloc(RelationView, names.len);
 542         errdefer allocator.free(relations);
 543         var relation_count: usize = 0;
 544         errdefer for (relations[0..relation_count]) |*relation_view| relation_view.deinit();
 545 
 546         const snapshots = try allocator.alloc(diff_mod.RelationSnapshot, names.len);
 547         errdefer allocator.free(snapshots);
 548 
 549         for (names, relations, snapshots) |name, *relation_view, *snapshot| {
 550             relation_view.* = try self.relationView(allocator, name);
 551             relation_count += 1;
 552             snapshot.* = relation_view.snapshot();
 553         }
 554 
 555         return .{
 556             .allocator = allocator,
 557             .relations = relations,
 558             .snapshots = snapshots,
 559             .conflicts = conflicts,
 560         };
 561     }
 562 
 563     pub fn databaseViewAtCommit(self: *const Connection, allocator: Allocator, history: *const history_mod.History, commit_hash: version.Hash) Error!DatabaseView {
 564         try self.ensureUsable();
 565         var root = try history.commitDatabaseRoot(allocator, commit_hash);
 566         defer root.deinit();
 567         return try databaseViewFromRoot(allocator, history, root);
 568     }
 569 
 570     pub fn diffRelation(self: *const Connection, allocator: Allocator, name: []const u8, other: diff_mod.RelationSnapshot) Error!diff_mod.RelationDiff {
 571         try self.ensureUsable();
 572         var local = try self.relationView(allocator, name);
 573         defer local.deinit();
 574         return try diff_mod.relation(allocator, local.snapshot(), other);
 575     }
 576 
 577     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 {
 578         try self.ensureUsable();
 579         var ours = try self.materializedWorkingValue(allocator);
 580         defer ours.deinit();
 581         var ours_conflicts = try history.conflictArtifacts(allocator, ours.root.conflicts);
 582         defer ours_conflicts.deinit();
 583         var result = try merge_mod.mergeDatabase(allocator, base, .{
 584             .value = &ours,
 585             .conflicts = .{
 586                 .root = ours_conflicts.root,
 587                 .artifacts = ours_conflicts.artifacts,
 588             },
 589         }, theirs);
 590         errdefer result.deinit();
 591         try self.materializeDatabaseMerge(allocator, history, &result, options.commit);
 592         var root = try result.value.root.clone(self.session.allocator);
 593         self.session.applyRoot(&root);
 594         return result;
 595     }
 596 
 597     pub fn mergeBranch(self: *Connection, allocator: Allocator, history: *history_mod.History, theirs: []const u8, options: MergeOptions) Error!merge_mod.DatabaseMerge {
 598         try self.ensureUsable();
 599         const theirs_ref = (try history.ref(theirs)) orelse return error.RefNotFound;
 600         const entries = try history.commitEntries(allocator);
 601         defer allocator.free(entries);
 602         const base_commit = (try branch_mod.mergeBase(allocator, entries, self.session.checkout.head, theirs_ref.target)) orelse return error.NoMergeBase;
 603         var base_view = try self.databaseViewAtCommit(allocator, history, base_commit);
 604         defer base_view.deinit();
 605         var theirs_view = try self.databaseViewAtCommit(allocator, history, theirs_ref.target);
 606         defer theirs_view.deinit();
 607         return try self.mergeDatabase(allocator, history, base_view.snapshot(), theirs_view.snapshot(), options);
 608     }
 609 
 610     fn replaceConflictRoot(self: *Connection, conflicts: version.ConflictRoot) Error!version.Hash {
 611         var root = try version.DatabaseRoot.initSorted(self.session.allocator, self.session.workingRoot().entries, conflicts);
 612         self.session.applyRoot(&root);
 613         return (try self.workingRoot());
 614     }
 615 
 616     pub fn conflictArtifacts(self: *const Connection, allocator: Allocator, history: *const history_mod.History) Error!history_mod.ConflictArtifacts {
 617         try self.ensureUsable();
 618         return try history.conflictArtifacts(allocator, self.session.workingRoot().conflicts);
 619     }
 620 
 621     pub fn resolveConflicts(self: *Connection, allocator: Allocator, history: *history_mod.History, resolved: []const version.Hash) Error!version.Hash {
 622         try self.ensureUsable();
 623         if (resolved.len == 0) return try self.workingRoot();
 624         var entries = try history.conflictEntries(allocator, self.session.workingRoot().conflicts);
 625         defer entries.deinit();
 626         for (resolved) |hash| {
 627             if (!hasConflictHash(entries.entries, hash)) return error.ConflictNotFound;
 628         }
 629 
 630         var remaining: std.ArrayList(version.ConflictEntry) = .empty;
 631         defer remaining.deinit(allocator);
 632         for (entries.entries) |entry| {
 633             if (!hasConflictHashValue(resolved, entry.hash)) try remaining.append(allocator, entry);
 634         }
 635 
 636         const root = try history.putConflictRoot(remaining.items);
 637         return try self.replaceConflictRoot(root);
 638     }
 639 
 640     pub fn checkout(self: *const Connection) Error!@import("branch.zig").Checkout {
 641         try self.ensureUsable();
 642         return self.session.checkout;
 643     }
 644 
 645     pub fn workingRoot(self: *const Connection) Error!version.Hash {
 646         try self.ensureUsable();
 647         return self.session.checkout.working.working;
 648     }
 649 
 650     fn relationViewFromRoot(allocator: Allocator, history: *const history_mod.History, root_hash: version.Hash) Error!RelationView {
 651         var root = try history.relationRoot(allocator, root_hash);
 652         errdefer root.deinit();
 653         const rows = try history.relationRows(allocator, root_hash);
 654         errdefer version.freeRelationRows(allocator, rows);
 655         return .{
 656             .allocator = allocator,
 657             .root = root,
 658             .rows = rows,
 659         };
 660     }
 661 
 662     fn databaseViewFromRoot(allocator: Allocator, history: *const history_mod.History, root: version.DatabaseRoot) Error!DatabaseView {
 663         var conflicts = try history.conflictArtifacts(allocator, root.conflicts);
 664         errdefer conflicts.deinit();
 665 
 666         const relations = try allocator.alloc(RelationView, root.entries.len);
 667         errdefer allocator.free(relations);
 668         var relation_count: usize = 0;
 669         errdefer for (relations[0..relation_count]) |*relation_view| relation_view.deinit();
 670 
 671         const snapshots = try allocator.alloc(diff_mod.RelationSnapshot, root.entries.len);
 672         errdefer allocator.free(snapshots);
 673 
 674         for (root.entries, relations, snapshots) |entry, *relation_view, *snapshot| {
 675             relation_view.* = try relationViewFromRoot(allocator, history, entry.hash);
 676             relation_count += 1;
 677             snapshot.* = relation_view.snapshot();
 678         }
 679 
 680         return .{
 681             .allocator = allocator,
 682             .relations = relations,
 683             .snapshots = snapshots,
 684             .conflicts = conflicts,
 685         };
 686     }
 687 
 688     fn materializeDatabaseValue(self: *Connection, allocator: Allocator, value: *const version.DatabaseValue, options: file.CommitOptions) Error!void {
 689         var plan = try DatabaseMaterialization.init(self, allocator, value);
 690         defer plan.deinit();
 691         try plan.apply(self, options);
 692     }
 693 
 694     fn materializeDatabaseMerge(
 695         self: *Connection,
 696         allocator: Allocator,
 697         history: *history_mod.History,
 698         result: *const merge_mod.DatabaseMerge,
 699         options: file.CommitOptions,
 700     ) Error!void {
 701         if (!version.same(result.value.root.conflicts, result.conflict_root.hash)) {
 702             return error.InvalidHistory;
 703         }
 704         try result.persistConflicts(history);
 705         try history.validateConflictRoot(result.value.root.conflicts);
 706         try self.materializeDatabaseValue(allocator, &result.value, options);
 707         try self.verifyMaterializedDatabaseRoot(allocator, &result.value.root);
 708     }
 709 
 710     fn publishStagedValue(self: *Connection, history: *history_mod.History, root: version.Hash) Error!void {
 711         const allocator = self.session.allocator;
 712         const working = self.session.workingRoot();
 713         if (!version.same(working.hash, root)) return error.InvalidHistory;
 714         try history.validateConflictRoot(working.conflicts);
 715         var live_root = try version.databaseRootMaintained(
 716             allocator,
 717             &self.catalog,
 718             working.conflicts,
 719         );
 720         defer live_root.deinit();
 721         if (!version.same(live_root.hash, working.hash)) return error.InvalidHistory;
 722 
 723         var batch = try history.beginWriteBatch();
 724         errdefer batch.deinit();
 725 
 726         var base_root: ?version.DatabaseRoot = null;
 727         defer if (base_root) |*owned| owned.deinit();
 728         base_root = history.commitDatabaseRoot(allocator, self.session.checkout.head) catch null;
 729 
 730         for (working.entries) |entry| {
 731             const base = self.publicationBase(&base_root, entry.name);
 732             const need = try history.relationRowsNeed(allocator, entry.hash, base);
 733 
 734             var state = try self.catalog.readRelation(allocator, entry.name);
 735             defer state.deinit();
 736             const live_key = try version.relationKey(
 737                 entry.name,
 738                 &state.handle,
 739                 state.relationStats(),
 740             );
 741             if (!version.same(live_key.hash, entry.hash)) return error.InvalidHistory;
 742             if (need == .none and history.hasRelationRoot(entry.hash)) continue;
 743 
 744             var relation_root = try version.relationRootMaintained(
 745                 allocator,
 746                 entry.name,
 747                 state.schema,
 748                 &state.handle,
 749                 state.relationStats(),
 750             );
 751             defer relation_root.deinit();
 752             try history.putRelationRoot(relation_root);
 753 
 754             switch (need) {
 755                 .none => {},
 756                 .full => {
 757                     var writer = try history.beginRelationRows(entry.hash);
 758                     defer writer.deinit();
 759                     try streamRelationRows(&writer, allocator, &state.handle, null);
 760                     try writer.finish();
 761                 },
 762                 .suffix => |plan| {
 763                     var writer = try history.beginRelationRowsSuffix(entry.hash, base.?, plan);
 764                     defer writer.deinit();
 765                     try streamRelationRows(&writer, allocator, &state.handle, plan.boundary);
 766                     try writer.finish();
 767                 },
 768             }
 769         }
 770         try history.putDatabaseRoot(working.*);
 771         try batch.finish();
 772     }
 773 
 774     fn streamRelationRows(
 775         writer: *history_mod.RelationRowsWriter,
 776         allocator: Allocator,
 777         handle: *const catalog_mod.RelationHandle,
 778         boundary: ?i64,
 779     ) Error!void {
 780         if (boundary) |value| {
 781             if (value == std.math.maxInt(i64)) return;
 782         }
 783         const start: ?i64 = if (boundary) |value| value + 1 else null;
 784         var scan: relation_mod.Scan = undefined;
 785         try handle.relation.scan(&scan, allocator, start, null);
 786         defer scan.deinit();
 787         while (try scan.next()) |row_entry| try writer.append(row_entry.rowid, row_entry.bytes);
 788     }
 789 
 790     fn publicationBase(self: *const Connection, base_root: *const ?version.DatabaseRoot, name: []const u8) ?history_mod.IncrementalBase {
 791         const root = base_root.* orelse return null;
 792         const edited = self.session.pendingEditRowids(root.hash, name) orelse return null;
 793         for (root.entries) |entry| {
 794             if (std.mem.eql(u8, entry.name, name)) return .{ .root = entry.hash, .edited = edited };
 795         }
 796         return null;
 797     }
 798 
 799     pub fn materializedWorkingValue(self: *Connection, allocator: Allocator) Error!version.DatabaseValue {
 800         try self.ensureUsable();
 801         const working = self.session.workingRoot();
 802         return try version.databaseValue(allocator, &self.catalog, working.conflicts);
 803     }
 804 };
 805 
 806 fn recoverFastForward(
 807     allocator: Allocator,
 808     database: *file.Database,
 809     history: *history_mod.History,
 810     catalog_options: catalog_mod.Options,
 811 ) Error!void {
 812     const active = history.fastForwardRecovery() orelse return;
 813     const id = active.id;
 814     const decision = active.decision;
 815     const selected_head = switch (decision) {
 816         .pending, .baseline => active.expected,
 817         .target => active.target,
 818     };
 819     const name = active.name;
 820     try database.beginCoordinator();
 821     var coordinator_active = true;
 822     var recovered = false;
 823     defer {
 824         if (coordinator_active and !database.requiresRecovery()) database.endCoordinator();
 825         if (!recovered) {
 826             database.poison();
 827             history.poison();
 828         }
 829     }
 830 
 831     const commit_value = try history.commitValue(selected_head);
 832     var value = try history.databaseValue(allocator, commit_value.root);
 833     defer value.deinit();
 834     try history.validateConflictRoot(value.root.conflicts);
 835     const catalog = try catalog_mod.Catalog.open(database, catalog_options);
 836     var live_root = try version.databaseRootMaintained(
 837         allocator,
 838         &catalog,
 839         value.root.conflicts,
 840     );
 841     var live_root_owned = true;
 842     defer if (live_root_owned) live_root.deinit();
 843     const checkout_value = branch_mod.checkout(.{
 844         .name = name,
 845         .target = selected_head,
 846     }, live_root.hash);
 847     var connection = Connection.init(
 848         catalog,
 849         session_mod.DatabaseSession.initWithRoot(allocator, checkout_value, &live_root),
 850     );
 851     live_root_owned = false;
 852     defer connection.deinit();
 853 
 854     if (!version.same(connection.session.workingRoot().hash, value.root.hash)) {
 855         var plan = try DatabaseMaterialization.init(&connection, allocator, &value);
 856         defer plan.deinit();
 857         try plan.apply(&connection, .{ .durability = .synced });
 858     } else {
 859         try database.syncWal();
 860     }
 861     try connection.verifyMaterializedDatabaseRoot(allocator, &value.root);
 862 
 863     var update = history_mod.FastForwardUpdate{ .history = history, .id = id };
 864     if (decision == .pending) try update.abort();
 865     try update.complete();
 866     database.endCoordinator();
 867     coordinator_active = false;
 868     recovered = true;
 869 }
 870 
 871 const PlannedRelation = struct {
 872     target: *const version.RelationValue,
 873     definition: catalog_mod.RelationDefinition,
 874     live: ?RelationView = null,
 875     edits: []relation_mod.Edit = &.{},
 876     prepared_stats: ?catalog_mod.PreparedRelationStats = null,
 877     created: ?catalog_mod.MaterializedRelation = null,
 878     clear_live_stats: bool = false,
 879 
 880     fn deinit(self: *PlannedRelation, allocator: Allocator) void {
 881         if (self.prepared_stats) |*prepared| prepared.deinit();
 882         if (self.edits.len != 0) allocator.free(self.edits);
 883         if (self.live) |*live| live.deinit();
 884         self.* = undefined;
 885     }
 886 };
 887 
 888 const DatabaseMaterialization = struct {
 889     allocator: Allocator,
 890     names: catalog_mod.RelationNames,
 891     relations: []PlannedRelation,
 892     relation_count: usize = 0,
 893 
 894     fn init(
 895         connection: *Connection,
 896         allocator: Allocator,
 897         value: *const version.DatabaseValue,
 898     ) Error!DatabaseMaterialization {
 899         try validateValue(value);
 900         var names = try connection.catalog.relationNames(allocator);
 901         errdefer names.deinit();
 902         const relations = try allocator.alloc(PlannedRelation, value.relations.len);
 903         var relation_count: usize = 0;
 904         errdefer {
 905             for (relations[0..relation_count]) |*relation| relation.deinit(allocator);
 906             if (relations.len != 0) allocator.free(relations);
 907         }
 908 
 909         for (value.relations, relations, 0..) |*target, *planned, relation_offset| {
 910             try validateTarget(value, target, relation_offset);
 911             planned.* = try planRelation(
 912                 connection,
 913                 allocator,
 914                 target,
 915                 names.names,
 916             );
 917             relation_count += 1;
 918         }
 919 
 920         return .{
 921             .allocator = allocator,
 922             .names = names,
 923             .relations = relations,
 924             .relation_count = relation_count,
 925         };
 926     }
 927 
 928     fn validateValue(value: *const version.DatabaseValue) Error!void {
 929         if (value.root.format != version.format_version or value.root.feature != 0) {
 930             return error.InvalidHistory;
 931         }
 932         if (value.root.entries.len != value.relations.len) return error.InvalidHistory;
 933         for (value.root.entries, 0..) |entry, index| {
 934             if (entry.name.len == 0) return error.InvalidHistory;
 935             if (index != 0 and
 936                 simd.order(Bytes, value.root.entries[index - 1].name, entry.name) != .lt)
 937             {
 938                 return error.InvalidHistory;
 939             }
 940         }
 941         const rebuilt = version.DatabaseRoot.init(
 942             value.root.entries,
 943             .{ .hash = value.root.conflicts },
 944         );
 945         if (!version.same(rebuilt.hash, value.root.hash)) return error.InvalidHistory;
 946     }
 947 
 948     fn validateTarget(
 949         value: *const version.DatabaseValue,
 950         target: *const version.RelationValue,
 951         relation_offset: usize,
 952     ) Error!void {
 953         for (value.relations[0..relation_offset]) |previous| {
 954             if (std.mem.eql(u8, previous.root.name, target.root.name)) {
 955                 return error.InvalidHistory;
 956             }
 957         }
 958         const entry = relationEntry(
 959             value.root.entries,
 960             target.root.name,
 961         ) orelse return error.InvalidHistory;
 962         if (!version.same(entry.hash, target.root.hash)) return error.InvalidHistory;
 963         if (!relationRowsStrictlySorted(target.rows)) return error.InvalidHistory;
 964     }
 965 
 966     fn planRelation(
 967         connection: *Connection,
 968         allocator: Allocator,
 969         target: *const version.RelationValue,
 970         names: []const []const u8,
 971     ) Error!PlannedRelation {
 972         var planned = PlannedRelation{
 973             .target = target,
 974             .definition = .{
 975                 .name = target.root.name,
 976                 .columns = target.root.schema_descriptor.columns,
 977                 .indexes = target.root.schema_descriptor.indexes,
 978             },
 979         };
 980         errdefer planned.deinit(allocator);
 981         if (target.root.catalog.format != catalog_mod.format_version) {
 982             return error.InvalidHistory;
 983         }
 984         catalog_mod.validateDefinition(planned.definition) catch |err| switch (err) {
 985             error.OutOfMemory => return err,
 986             else => return error.InvalidHistory,
 987         };
 988         planned.prepared_stats = try validateRelationIdentity(
 989             connection,
 990             allocator,
 991             target,
 992             planned.definition,
 993         );
 994         if (relationNameExists(names, target.root.name)) {
 995             planned.live = try connection.relationView(allocator, target.root.name);
 996             const live = &planned.live.?;
 997             if (!version.same(live.root.schema, target.root.schema)) {
 998                 return error.UnsupportedCheckoutRoot;
 999             }
1000             const stats_mismatch = !version.same(
1001                 live.root.stats.hash,
1002                 target.root.stats.hash,
1003             );
1004             if (stats_mismatch and relationRootHasStats(&target.root)) {
1005                 return error.UnsupportedCheckoutRoot;
1006             }
1007             planned.clear_live_stats = stats_mismatch;
1008             planned.edits = try relationMaterializationEdits(
1009                 allocator,
1010                 live.snapshot(),
1011                 target,
1012             );
1013         } else {
1014             planned.edits = try relationPutEdits(allocator, target.rows);
1015         }
1016         return planned;
1017     }
1018 
1019     fn validateRelationIdentity(
1020         connection: *Connection,
1021         allocator: Allocator,
1022         target: *const version.RelationValue,
1023         definition: catalog_mod.RelationDefinition,
1024     ) Error!?catalog_mod.PreparedRelationStats {
1025         if (!relationRootHasStats(&target.root)) {
1026             var rebuilt = version.relationRootFromRows(
1027                 allocator,
1028                 &target.root,
1029                 target.rows,
1030             ) catch |err| switch (err) {
1031                 error.OutOfMemory => return err,
1032                 else => return error.InvalidHistory,
1033             };
1034             defer rebuilt.deinit();
1035             if (!version.same(rebuilt.hash, target.root.hash)) return error.InvalidHistory;
1036             return null;
1037         }
1038         const table_summary = target.root.stats.table orelse return error.InvalidHistory;
1039         if (target.root.stats.indexes != target.root.indexes.len) {
1040             return error.InvalidHistory;
1041         }
1042         var index_summaries: [relation_mod.max_indexes]tree.Summary = undefined;
1043         if (target.root.indexes.len > index_summaries.len) return error.InvalidHistory;
1044         for (
1045             target.root.indexes,
1046             index_summaries[0..target.root.indexes.len],
1047         ) |index_root, *summary| {
1048             summary.* = index_root.map.summary;
1049         }
1050         const puts = try relationPuts(allocator, target.rows);
1051         defer if (puts.len != 0) allocator.free(puts);
1052         var prepared = connection.catalog.prepareRelationStats(
1053             allocator,
1054             definition,
1055             puts,
1056             table_summary,
1057             index_summaries[0..target.root.indexes.len],
1058         ) catch |err| switch (err) {
1059             error.OutOfMemory => return err,
1060             else => return error.InvalidHistory,
1061         };
1062         errdefer prepared.deinit();
1063         var rebuilt = version.relationRootFromRowsWithStats(
1064             allocator,
1065             &target.root,
1066             target.rows,
1067             &prepared.stats,
1068         ) catch |err| switch (err) {
1069             error.OutOfMemory => return err,
1070             else => return error.InvalidHistory,
1071         };
1072         defer rebuilt.deinit();
1073         if (!version.same(rebuilt.hash, target.root.hash)) return error.InvalidHistory;
1074         return prepared;
1075     }
1076 
1077     fn deinit(self: *DatabaseMaterialization) void {
1078         for (self.relations[0..self.relation_count]) |*relation| relation.deinit(self.allocator);
1079         if (self.relations.len != 0) self.allocator.free(self.relations);
1080         self.names.deinit();
1081         self.* = undefined;
1082     }
1083 
1084     fn apply(
1085         self: *DatabaseMaterialization,
1086         connection: *Connection,
1087         options: file.CommitOptions,
1088     ) Error!void {
1089         var materialization = try connection.catalog.beginMaterialization(self.allocator);
1090         defer materialization.deinit();
1091 
1092         for (self.names.names) |name| {
1093             if (!plannedRelationExists(self.relations[0..self.relation_count], name)) {
1094                 try materialization.dropRelation(name);
1095             }
1096         }
1097 
1098         for (self.relations[0..self.relation_count]) |*planned| {
1099             if (planned.live != null) continue;
1100             planned.created = try materialization.createRelation(
1101                 planned.definition,
1102                 if (planned.prepared_stats) |*prepared| prepared else null,
1103             );
1104         }
1105 
1106         for (self.relations[0..self.relation_count]) |*planned| {
1107             if (planned.clear_live_stats) {
1108                 try materialization.clearRelationStats(planned.target.root.name);
1109             }
1110             if (planned.edits.len != 0) {
1111                 if (planned.live) |*live| {
1112                     try live.handle.?.relation.applyEditsIn(
1113                         self.allocator,
1114                         materialization.treeWrite(),
1115                         planned.edits,
1116                     );
1117                 } else {
1118                     try planned.created.?.relation.applyEditsIn(
1119                         self.allocator,
1120                         materialization.treeWrite(),
1121                         planned.edits,
1122                     );
1123                 }
1124             }
1125             if (planned.created) |*created| {
1126                 if (planned.prepared_stats) |*prepared| {
1127                     try materialization.refreshRelationStats(planned.definition, created, prepared);
1128                 }
1129             }
1130         }
1131 
1132         _ = try materialization.commit(options);
1133     }
1134 };
1135 
1136 fn relationNameExists(names: []const []const u8, target: []const u8) bool {
1137     for (names) |name| {
1138         if (std.mem.eql(u8, name, target)) return true;
1139     }
1140     return false;
1141 }
1142 
1143 fn relationEntry(entries: []const version.RelationEntry, name: []const u8) ?version.RelationEntry {
1144     for (entries) |entry| {
1145         if (std.mem.eql(u8, entry.name, name)) return entry;
1146     }
1147     return null;
1148 }
1149 
1150 fn relationRootHasStats(root: *const version.RelationRoot) bool {
1151     return !version.same(root.stats.hash, version.emptyHash("sql.stats.none"));
1152 }
1153 
1154 fn plannedRelationExists(relations: []const PlannedRelation, name: []const u8) bool {
1155     for (relations) |relation| {
1156         if (std.mem.eql(u8, relation.target.root.name, name)) return true;
1157     }
1158     return false;
1159 }
1160 
1161 fn relationRowsStrictlySorted(rows: []const version.RelationRow) bool {
1162     var previous: ?i64 = null;
1163     for (rows) |row_value| {
1164         if (previous) |rowid| {
1165             if (row_value.rowid <= rowid) return false;
1166         }
1167         previous = row_value.rowid;
1168     }
1169     return true;
1170 }
1171 
1172 fn relationPuts(
1173     allocator: Allocator,
1174     rows: []const version.RelationRow,
1175 ) Allocator.Error![]relation_mod.Edit.Put {
1176     const puts = try allocator.alloc(relation_mod.Edit.Put, rows.len);
1177     for (rows, puts) |row_value, *put| {
1178         put.* = .{
1179             .rowid = row_value.rowid,
1180             .bytes = row_value.bytes,
1181         };
1182     }
1183     return puts;
1184 }
1185 
1186 fn relationPutEdits(
1187     allocator: Allocator,
1188     rows: []const version.RelationRow,
1189 ) Allocator.Error![]relation_mod.Edit {
1190     const edits = try allocator.alloc(relation_mod.Edit, rows.len);
1191     for (rows, edits) |row_value, *edit| {
1192         edit.* = .{ .put = .{
1193             .rowid = row_value.rowid,
1194             .bytes = row_value.bytes,
1195         } };
1196     }
1197     return edits;
1198 }
1199 
1200 fn relationMaterializationEdits(
1201     allocator: Allocator,
1202     live: diff_mod.RelationSnapshot,
1203     target: *const version.RelationValue,
1204 ) Error![]relation_mod.Edit {
1205     var result = try diff_mod.relation(allocator, live, .{
1206         .root = &target.root,
1207         .rows = .{ .materialized = target.rows },
1208     });
1209     defer result.deinit();
1210     if (result.schema_changed) return error.UnsupportedCheckoutRoot;
1211 
1212     var edits: std.ArrayList(relation_mod.Edit) = .empty;
1213     errdefer edits.deinit(allocator);
1214     for (result.changes) |change| {
1215         switch (change.kind) {
1216             .added, .modified => try edits.append(allocator, .{ .put = .{
1217                 .rowid = change.rowid,
1218                 .bytes = relationRowBytes(target.rows, change.rowid) orelse
1219                     return error.InvalidHistory,
1220             } }),
1221             .removed => try edits.append(allocator, .{ .delete = change.rowid }),
1222             .schema => return error.UnsupportedCheckoutRoot,
1223         }
1224     }
1225     return try edits.toOwnedSlice(allocator);
1226 }
1227 
1228 fn relationRowBytes(rows: []const version.RelationRow, rowid: i64) ?[]const u8 {
1229     for (rows) |row_value| {
1230         if (row_value.rowid == rowid) return row_value.bytes;
1231         if (row_value.rowid > rowid) return null;
1232     }
1233     return null;
1234 }
1235 
1236 fn publishDatabaseValue(history: *history_mod.History, value: *const version.DatabaseValue) history_mod.Error!void {
1237     try history.putDatabaseValue(value);
1238 }
1239 
1240 test "connection commits reuse history chunks for append shaped edits" {
1241     var tmp = std.testing.tmpDir(.{});
1242     defer tmp.cleanup();
1243 
1244     var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
1245         .paths = .{ .database = "chunks.db", .wal = "chunks.wal" },
1246         .header = testingHeader(),
1247     });
1248     defer database.deinit();
1249     try database.reserve(.{ .wal_frames = 2048 });
1250 
1251     var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "chunks.history", .recovery = .reject });
1252     defer history.deinit();
1253 
1254     var connection = try Connection.create(std.testing.allocator, &database, &history, .{});
1255     defer connection.deinit();
1256 
1257     var created = try connection.execute(std.testing.allocator, "CREATE TABLE items (name)", .{ .durability = .buffered });
1258     created.deinit(std.testing.allocator);
1259     try connection.stage();
1260     _ = try connection.commit(&history);
1261 
1262     var rowid: i64 = 1;
1263     var statement_buffer: [128]u8 = undefined;
1264     while (rowid <= 600) : (rowid += 1) {
1265         const source = try std.fmt.bufPrint(&statement_buffer, "INSERT INTO items VALUES ({d}, 'row-{d}')", .{ rowid, rowid });
1266         var inserted = try connection.execute(std.testing.allocator, source, .{ .durability = .buffered });
1267         inserted.deinit(std.testing.allocator);
1268     }
1269     try connection.stage();
1270     _ = try connection.commit(&history);
1271     const seeded_chunks = history.row_chunks.items.len;
1272     try std.testing.expect(seeded_chunks >= 4);
1273 
1274     while (rowid <= 603) : (rowid += 1) {
1275         const source = try std.fmt.bufPrint(&statement_buffer, "INSERT INTO items VALUES ({d}, 'row-{d}')", .{ rowid, rowid });
1276         var inserted = try connection.execute(std.testing.allocator, source, .{ .durability = .buffered });
1277         inserted.deinit(std.testing.allocator);
1278     }
1279     try connection.stage();
1280     try expectAppendPrefixReuse(&connection, &history);
1281     const append_commit = try connection.commit(&history);
1282     const append_chunks = history.row_chunks.items.len;
1283     try std.testing.expect(append_chunks - seeded_chunks <= 2);
1284 
1285     var update = try connection.execute(std.testing.allocator, "UPDATE items SET name = 'renamed' WHERE rowid = 5", .{ .durability = .buffered });
1286     update.deinit(std.testing.allocator);
1287     try connection.stage();
1288     const update_commit = try connection.commit(&history);
1289     const update_chunks = history.row_chunks.items.len;
1290     try std.testing.expect(update_chunks - append_chunks <= 3);
1291 
1292     var remove = try connection.execute(std.testing.allocator, "DELETE FROM items WHERE rowid = 300", .{ .durability = .buffered });
1293     remove.deinit(std.testing.allocator);
1294     try connection.stage();
1295     const delete_commit = try connection.commit(&history);
1296 
1297     for ([_]version.Hash{ append_commit, update_commit, delete_commit }) |commit_hash| {
1298         const commit_value = try history.commitValue(commit_hash);
1299         var value = try history.databaseValue(std.testing.allocator, commit_value.root);
1300         value.deinit();
1301     }
1302 
1303     var final = try history.databaseValue(std.testing.allocator, (try connection.checkout()).working.working);
1304     defer final.deinit();
1305     const relation = final.findRelation("items") orelse return error.TestUnexpectedResult;
1306     try std.testing.expectEqual(@as(usize, 602), relation.rows.len);
1307 }
1308 
1309 fn expectAppendPrefixReuse(connection: *Connection, history: *history_mod.History) !void {
1310     const allocator = std.testing.allocator;
1311     var base_root: ?version.DatabaseRoot = try history.commitDatabaseRoot(
1312         allocator,
1313         connection.session.checkout.head,
1314     );
1315     defer base_root.?.deinit();
1316     const base = connection.publicationBase(&base_root, "items").?;
1317     const working = connection.session.workingRoot();
1318     const entry = relationEntry(working.entries, "items").?;
1319     const need = try history.relationRowsNeed(allocator, entry.hash, base);
1320     try std.testing.expect(need == .suffix);
1321     try std.testing.expect(need.suffix.reused > 0);
1322 }
1323 
1324 test "local connection commits the maintained database without history" {
1325     var tmp = std.testing.tmpDir(.{});
1326     defer tmp.cleanup();
1327 
1328     {
1329         var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
1330             .paths = .{ .database = "local.db", .wal = "local.wal" },
1331             .header = testingHeader(),
1332         });
1333         defer database.deinit();
1334         try database.reserve(.{ .wal_frames = 512 });
1335 
1336         var connection = try Connection.openLocal(std.testing.allocator, &database, .{});
1337         defer connection.deinit();
1338         try std.testing.expect(!(try connection.checkout()).working.dirty());
1339 
1340         var created = try connection.execute(
1341             std.testing.allocator,
1342             "CREATE TABLE items (name)",
1343             .{ .durability = .buffered },
1344         );
1345         created.deinit(std.testing.allocator);
1346         try connection.stage();
1347         _ = try connection.commitLocal();
1348         try std.testing.expect(!(try connection.checkout()).working.dirty());
1349 
1350         var inserted = try connection.execute(
1351             std.testing.allocator,
1352             "INSERT INTO items VALUES (1, 'local')",
1353             .{ .durability = .buffered },
1354         );
1355         inserted.deinit(std.testing.allocator);
1356         try connection.stage();
1357         _ = try connection.commitLocal();
1358         try database.syncWal();
1359     }
1360 
1361     var reopened_database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
1362         .paths = .{ .database = "local.db", .wal = "local.wal" },
1363         .header = testingHeader(),
1364     });
1365     defer reopened_database.deinit();
1366     var reopened = try Connection.openLocal(std.testing.allocator, &reopened_database, .{});
1367     defer reopened.deinit();
1368     var selected = try reopened.execute(
1369         std.testing.allocator,
1370         "SELECT name FROM items WHERE rowid = 1",
1371         .{ .durability = .buffered },
1372     );
1373     defer selected.deinit(std.testing.allocator);
1374     const view = try row.View.init(selected.nextRow().?);
1375     try std.testing.expectEqualStrings("local", (try view.column(0)).text);
1376     try std.testing.expect(selected.nextRow() == null);
1377 }
1378 
1379 test "connection executes statement writes through its database session" {
1380     var tmp = std.testing.tmpDir(.{});
1381     defer tmp.cleanup();
1382 
1383     var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
1384         .paths = .{ .database = "connection.db", .wal = "connection.wal" },
1385         .header = testingHeader(),
1386     });
1387     defer database.deinit();
1388     try database.reserve(.{ .wal_frames = 640 });
1389 
1390     var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "connection.history", .recovery = .reject });
1391     defer history.deinit();
1392 
1393     var connection = try Connection.create(std.testing.allocator, &database, &history, .{});
1394     defer connection.deinit();
1395     try std.testing.expect(!(try connection.checkout()).working.dirty());
1396 
1397     var created = try connection.execute(std.testing.allocator, "CREATE TABLE items (name DEFAULT 'missing', score, INDEX items_score (score))", .{ .durability = .buffered });
1398     defer created.deinit(std.testing.allocator);
1399     const create_flush = switch (created) {
1400         .catalog => |flush| flush,
1401         else => return error.UnsupportedStatement,
1402     };
1403     try std.testing.expect(version.same(create_flush.database, (try connection.workingRoot())));
1404     try std.testing.expect((try connection.checkout()).working.dirty());
1405 
1406     var insert = try connection.prepare(std.testing.allocator, "INSERT INTO items (rowid, name, score) VALUES (?1, ?2, ?3)");
1407     defer insert.deinit();
1408     try insert.bind(1, .{ .integer = 4 });
1409     try insert.bind(2, .{ .text = "Ada" });
1410     try insert.bind(3, .{ .integer = 7 });
1411     var inserted = try connection.executePrepared(&insert, std.testing.allocator, .{ .durability = .buffered });
1412     defer inserted.deinit(std.testing.allocator);
1413     try std.testing.expect(version.same(inserted.mutation.database, (try connection.workingRoot())));
1414 
1415     var selected = try connection.execute(std.testing.allocator, "SELECT name, score, rowid FROM items WHERE score = 7", .{ .durability = .buffered });
1416     defer selected.deinit(std.testing.allocator);
1417     try std.testing.expectEqual(@as(usize, 1), selected.rowCount());
1418     const bytes = selected.nextRow().?;
1419     const view = try row.View.init(bytes);
1420     try std.testing.expectEqualStrings("Ada", (try view.column(0)).text);
1421     try std.testing.expectEqual(@as(i64, 7), (try view.column(1)).integer);
1422     try std.testing.expectEqual(@as(i64, 4), (try view.column(2)).integer);
1423 
1424     try connection.stage();
1425     const commit_hash = try connection.commit(&history);
1426     try std.testing.expect(version.same(commit_hash, (try history.ref("main")).?.target));
1427     try std.testing.expect(!(try connection.checkout()).working.dirty());
1428     var committed_root = try history.commitDatabaseRoot(std.testing.allocator, commit_hash);
1429     defer committed_root.deinit();
1430     try std.testing.expect(version.same(committed_root.hash, (try connection.workingRoot())));
1431     try std.testing.expect(committed_root.entries.len > 0);
1432     for (committed_root.entries) |entry| {
1433         var relation_root = try history.relationRoot(std.testing.allocator, entry.hash);
1434         defer relation_root.deinit();
1435         try std.testing.expect(version.same(entry.hash, relation_root.hash));
1436         try std.testing.expectEqualStrings(entry.name, relation_root.name);
1437         const relation_rows = try history.relationRows(std.testing.allocator, entry.hash);
1438         defer version.freeRelationRows(std.testing.allocator, relation_rows);
1439         try std.testing.expectEqual(@as(usize, 1), relation_rows.len);
1440         try std.testing.expectEqual(@as(i64, 4), relation_rows[0].rowid);
1441     }
1442 
1443     var reopened = try Connection.open(std.testing.allocator, &database, &history, .{});
1444     defer reopened.deinit();
1445     try std.testing.expect(version.same(commit_hash, (try reopened.checkout()).head));
1446     try std.testing.expect(version.same((try connection.workingRoot()), (try reopened.workingRoot())));
1447     try std.testing.expect(version.same((try reopened.workingRoot()), reopened.session.workingRoot().hash));
1448 }
1449 
1450 test "connection publishes catalog session values on commit" {
1451     var tmp = std.testing.tmpDir(.{});
1452     defer tmp.cleanup();
1453 
1454     var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
1455         .paths = .{ .database = "connection-catalog-commit.db", .wal = "connection-catalog-commit.wal" },
1456         .header = testingHeader(),
1457     });
1458     defer database.deinit();
1459     try database.reserve(.{ .wal_frames = 512 });
1460 
1461     var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "connection-catalog-commit.history", .recovery = .reject });
1462     defer history.deinit();
1463 
1464     var connection = try Connection.create(std.testing.allocator, &database, &history, .{});
1465     defer connection.deinit();
1466 
1467     var created = try connection.execute(std.testing.allocator, "CREATE TABLE items (name DEFAULT 'missing', score, INDEX items_score (score))", .{ .durability = .buffered });
1468     defer created.deinit(std.testing.allocator);
1469     const create_flush = switch (created) {
1470         .catalog => |flush| flush,
1471         else => return error.UnsupportedStatement,
1472     };
1473     const working_root = connection.session.workingRoot();
1474     try std.testing.expect(version.same(create_flush.database, working_root.hash));
1475     try std.testing.expectEqual(@as(usize, 1), working_root.entries.len);
1476     try std.testing.expectError(error.DatabaseRootNotFound, history.databaseRoot(std.testing.allocator, create_flush.database));
1477 
1478     try connection.stage();
1479     const commit_hash = try connection.commit(&history);
1480     try std.testing.expect(version.same(create_flush.database, connection.session.workingRoot().hash));
1481     var committed = try history.commitDatabaseRoot(std.testing.allocator, commit_hash);
1482     defer committed.deinit();
1483     try std.testing.expect(version.same(create_flush.database, committed.hash));
1484     try std.testing.expectEqual(@as(usize, 1), committed.entries.len);
1485     var relation_root = try history.relationRoot(std.testing.allocator, committed.entries[0].hash);
1486     defer relation_root.deinit();
1487     try std.testing.expectEqualStrings("items", relation_root.name);
1488     const rows = try history.relationRows(std.testing.allocator, committed.entries[0].hash);
1489     defer version.freeRelationRows(std.testing.allocator, rows);
1490     try std.testing.expectEqual(@as(usize, 0), rows.len);
1491 }
1492 
1493 test "connection open seeds first write from committed database value" {
1494     var tmp = std.testing.tmpDir(.{});
1495     defer tmp.cleanup();
1496 
1497     var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
1498         .paths = .{ .database = "connection-seed-open.db", .wal = "connection-seed-open.wal" },
1499         .header = testingHeader(),
1500     });
1501     defer database.deinit();
1502     try database.reserve(.{ .wal_frames = 960 });
1503 
1504     var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "connection-seed-open.history", .recovery = .reject });
1505     defer history.deinit();
1506 
1507     var author = try Connection.create(std.testing.allocator, &database, &history, .{});
1508     defer author.deinit();
1509     try executeStatement(&author, "CREATE TABLE items (name)");
1510     try executeStatement(&author, "CREATE TABLE users (name)");
1511     try executeStatement(&author, "INSERT INTO items (rowid, name) VALUES (1, 'base')");
1512     try author.stage();
1513     const base_commit = try author.commit(&history);
1514 
1515     var reopened = try Connection.open(std.testing.allocator, &database, &history, .{});
1516     defer reopened.deinit();
1517     try std.testing.expect(version.same(base_commit, (try reopened.checkout()).head));
1518     const seeded_root = reopened.session.workingRoot();
1519     try std.testing.expect(version.same((try reopened.workingRoot()), seeded_root.hash));
1520     const seeded_items_hash = testEntryHash(seeded_root, "items");
1521 
1522     var live_items = try reopened.catalog.openRelation(std.testing.allocator, "items");
1523     defer live_items.deinit();
1524     _ = try live_items.relation.put(std.testing.allocator, 9, &.{.{ .text = "live-only" }}, .{ .durability = .buffered });
1525     var live_root = try version.databaseRootMaintained(
1526         std.testing.allocator,
1527         &reopened.catalog,
1528         version.ConflictRoot.empty().hash,
1529     );
1530     defer live_root.deinit();
1531     try std.testing.expect(!version.same(seeded_items_hash, testEntryHash(&live_root, "items")));
1532 
1533     try executeStatement(&reopened, "INSERT INTO users (rowid, name) VALUES (2, 'ada')");
1534     const flushed_root = reopened.session.workingRoot();
1535     try std.testing.expect(version.same(seeded_items_hash, testEntryHash(flushed_root, "items")));
1536     try std.testing.expect(version.same(testEntryHash(&live_root, "users"), testEntryHash(flushed_root, "users")) == false);
1537 }
1538 
1539 test "connection manages branch checkout fast forward and merge commits" {
1540     var tmp = std.testing.tmpDir(.{});
1541     defer tmp.cleanup();
1542 
1543     var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
1544         .paths = .{ .database = "connection-branch.db", .wal = "connection-branch.wal" },
1545         .header = testingHeader(),
1546     });
1547     defer database.deinit();
1548     try database.reserve(.{ .wal_frames = 960 });
1549 
1550     var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "connection-branch.history", .recovery = .reject });
1551     defer history.deinit();
1552 
1553     var connection = try Connection.create(std.testing.allocator, &database, &history, .{});
1554     defer connection.deinit();
1555     try executeStatement(&connection, "CREATE TABLE items (name)");
1556     try executeStatement(&connection, "INSERT INTO items (rowid, name) VALUES (1, 'base')");
1557     try connection.stage();
1558     const base_commit = try connection.commit(&history);
1559     _ = try connection.createBranch(&history, "side");
1560     _ = try connection.createBranch(&history, "behind");
1561 
1562     try executeStatement(&connection, "INSERT INTO items (rowid, name) VALUES (2, 'main')");
1563     try connection.stage();
1564     const main_commit = try connection.commit(&history);
1565     try std.testing.expect(version.same(base_commit, (try connection.mergeBase(std.testing.allocator, &history, "side")).?));
1566 
1567     try connection.checkoutBranch(std.testing.allocator, &history, "side");
1568     try std.testing.expect(version.same(base_commit, (try connection.checkout()).head));
1569     var main_row_after_side_checkout = try connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 2", .{ .durability = .buffered });
1570     defer main_row_after_side_checkout.deinit(std.testing.allocator);
1571     try std.testing.expectEqual(@as(usize, 0), main_row_after_side_checkout.rowCount());
1572     try executeStatement(&connection, "INSERT INTO items (rowid, name) VALUES (3, 'side')");
1573     try connection.stage();
1574     const side_commit = try connection.commit(&history);
1575     try std.testing.expect(version.same(base_commit, (try connection.mergeBase(std.testing.allocator, &history, "main")).?));
1576 
1577     try executeStatement(&connection, "INSERT INTO items (rowid, name) VALUES (4, 'merged')");
1578     try connection.stage();
1579     const merge_commit = try connection.mergeCommit(&history, main_commit);
1580     const entries = try history.commitEntries(std.testing.allocator);
1581     defer std.testing.allocator.free(entries);
1582     var merge_entry: ?branch_mod.CommitEntry = null;
1583     for (entries) |entry| {
1584         if (version.same(entry.hash, merge_commit)) merge_entry = entry;
1585     }
1586     try std.testing.expect(merge_entry != null);
1587     try std.testing.expectEqual(@as(usize, 2), merge_entry.?.parents.len);
1588     try std.testing.expect(version.same(side_commit, merge_entry.?.parents[0]));
1589     try std.testing.expect(version.same(main_commit, merge_entry.?.parents[1]));
1590 
1591     try connection.checkoutBranch(std.testing.allocator, &history, "behind");
1592     try connection.fastForwardBranch(std.testing.allocator, &history, merge_commit);
1593     try std.testing.expect(version.same(merge_commit, (try connection.checkout()).head));
1594     try std.testing.expect(version.same((try history.ref("behind")).?.target, merge_commit));
1595     var side_row_after_fast_forward = try connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 3", .{ .durability = .buffered });
1596     defer side_row_after_fast_forward.deinit(std.testing.allocator);
1597     try std.testing.expectEqual(@as(usize, 1), side_row_after_fast_forward.rowCount());
1598 }
1599 
1600 test "connection checkout creates missing relations from committed schema descriptors" {
1601     var tmp = std.testing.tmpDir(.{});
1602     defer tmp.cleanup();
1603 
1604     var author_database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
1605         .paths = .{ .database = "checkout-create-author.db", .wal = "checkout-create-author.wal" },
1606         .header = testingHeader(),
1607     });
1608     defer author_database.deinit();
1609     try author_database.reserve(.{ .wal_frames = 960 });
1610 
1611     var target_database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
1612         .paths = .{ .database = "checkout-create-target.db", .wal = "checkout-create-target.wal" },
1613         .header = .{
1614             .sequence = 3904,
1615             .salt = .{ .first = 0x1357_3904, .second = 0x2468_3904 },
1616         },
1617     });
1618     defer target_database.deinit();
1619     try target_database.reserve(.{ .wal_frames = 960 });
1620 
1621     var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "checkout-create.history", .recovery = .reject });
1622     defer history.deinit();
1623 
1624     var author = try Connection.create(std.testing.allocator, &author_database, &history, .{});
1625     defer author.deinit();
1626     _ = try author.createBranch(&history, "side");
1627     try author.checkoutBranch(std.testing.allocator, &history, "side");
1628     try executeStatement(&author, "CREATE TABLE notes (title DEFAULT 'untitled' COLLATE nocase, body, INDEX notes_title (title))");
1629     try executeStatement(&author, "INSERT INTO notes (rowid, title, body) VALUES (9, 'First', 'side note')");
1630     try author.stage();
1631     const side_commit = try author.commit(&history);
1632 
1633     var target = try Connection.open(std.testing.allocator, &target_database, &history, .{});
1634     defer target.deinit();
1635     try target.checkoutBranch(std.testing.allocator, &history, "side");
1636     try std.testing.expect(version.same(side_commit, (try target.checkout()).head));
1637     try std.testing.expect(version.same((try history.ref("side")).?.target, (try target.checkout()).head));
1638 
1639     var found = try target.execute(std.testing.allocator, "SELECT body FROM notes WHERE title = 'first'", .{ .durability = .buffered });
1640     defer found.deinit(std.testing.allocator);
1641     try std.testing.expectEqual(@as(usize, 1), found.rowCount());
1642     const found_view = try row.View.init(found.nextRow().?);
1643     try std.testing.expectEqualStrings("side note", (try found_view.column(0)).text);
1644 
1645     try executeStatement(&target, "INSERT INTO notes (rowid, body) VALUES (10, 'uses default')");
1646     var defaulted = try target.execute(std.testing.allocator, "SELECT title FROM notes WHERE rowid = 10", .{ .durability = .buffered });
1647     defer defaulted.deinit(std.testing.allocator);
1648     try std.testing.expectEqual(@as(usize, 1), defaulted.rowCount());
1649     const defaulted_view = try row.View.init(defaulted.nextRow().?);
1650     try std.testing.expectEqualStrings("untitled", (try defaulted_view.column(0)).text);
1651 }
1652 
1653 test "connection checkout recreates stats for missing analyzed relations" {
1654     var tmp = std.testing.tmpDir(.{});
1655     defer tmp.cleanup();
1656 
1657     var author_database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
1658         .paths = .{ .database = "checkout-stats-author.db", .wal = "checkout-stats-author.wal" },
1659         .header = testingHeader(),
1660     });
1661     defer author_database.deinit();
1662     try author_database.reserve(.{ .wal_frames = 1280 });
1663 
1664     var target_database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
1665         .paths = .{ .database = "checkout-stats-target.db", .wal = "checkout-stats-target.wal" },
1666         .header = .{
1667             .sequence = 4904,
1668             .salt = .{ .first = 0x1357_4904, .second = 0x2468_4904 },
1669         },
1670     });
1671     defer target_database.deinit();
1672     try target_database.reserve(.{ .wal_frames = 1280 });
1673 
1674     var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "checkout-stats.history", .recovery = .reject });
1675     defer history.deinit();
1676 
1677     var author = try Connection.create(std.testing.allocator, &author_database, &history, .{});
1678     defer author.deinit();
1679     _ = try author.createBranch(&history, "side");
1680     try author.checkoutBranch(std.testing.allocator, &history, "side");
1681     try executeStatement(&author, "CREATE TABLE notes (title COLLATE nocase, body, INDEX notes_title (title))");
1682     try executeStatement(&author, "INSERT INTO notes (rowid, title, body) VALUES (1, 'Alpha', 'first')");
1683     try executeStatement(&author, "INSERT INTO notes (rowid, title, body) VALUES (2, 'Beta', 'second')");
1684     try executeStatement(&author, "INSERT INTO notes (rowid, title, body) VALUES (3, 'Beta', 'third')");
1685     try executeStatement(&author, "ANALYZE notes");
1686     try author.stage();
1687     const side_commit = try author.commit(&history);
1688 
1689     var target = try Connection.open(std.testing.allocator, &target_database, &history, .{});
1690     defer target.deinit();
1691     try target.checkoutBranch(std.testing.allocator, &history, "side");
1692     try std.testing.expect(version.same(side_commit, (try target.checkout()).head));
1693 
1694     var stats = (try target.catalog.relationStats(std.testing.allocator, "notes")).?;
1695     defer stats.deinit();
1696     try std.testing.expectEqual(@as(usize, 3), stats.table.entries);
1697     try std.testing.expectEqual(@as(usize, 1), stats.indexes.len);
1698     const index_stats = stats.index("notes_title").?;
1699     try std.testing.expectEqual(@as(usize, 3), index_stats.summary.entries);
1700     try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.distinct_values);
1701     try std.testing.expectEqual(@as(usize, 2), index_stats.distribution.max_equal);
1702 
1703     var found = try target.execute(std.testing.allocator, "SELECT body FROM notes WHERE title = 'beta'", .{ .durability = .buffered });
1704     defer found.deinit(std.testing.allocator);
1705     try std.testing.expectEqual(@as(usize, 2), found.rowCount());
1706 }
1707 
1708 test "connection checkout drops relations absent from target root" {
1709     var tmp = std.testing.tmpDir(.{});
1710     defer tmp.cleanup();
1711 
1712     var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
1713         .paths = .{ .database = "checkout-drop.db", .wal = "checkout-drop.wal" },
1714         .header = testingHeader(),
1715     });
1716     defer database.deinit();
1717     try database.reserve(.{ .wal_frames = 960 });
1718 
1719     var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "checkout-drop.history", .recovery = .reject });
1720     defer history.deinit();
1721 
1722     var connection = try Connection.create(std.testing.allocator, &database, &history, .{});
1723     defer connection.deinit();
1724     try executeStatement(&connection, "CREATE TABLE items (name)");
1725     try executeStatement(&connection, "INSERT INTO items (rowid, name) VALUES (7, 'main')");
1726     try connection.stage();
1727     const main_commit = try connection.commit(&history);
1728     _ = try connection.createBranch(&history, "side");
1729 
1730     try connection.checkoutBranch(std.testing.allocator, &history, "side");
1731     try executeStatement(&connection, "CREATE TABLE notes (title, INDEX notes_title (title))");
1732     try executeStatement(&connection, "INSERT INTO notes (rowid, title) VALUES (1, 'side')");
1733     try connection.stage();
1734     const side_commit = try connection.commit(&history);
1735     try std.testing.expect(version.same(side_commit, (try connection.checkout()).head));
1736 
1737     var names_on_side = try connection.catalog.relationNames(std.testing.allocator);
1738     defer names_on_side.deinit();
1739     try std.testing.expectEqual(@as(usize, 2), names_on_side.names.len);
1740 
1741     try connection.checkoutBranch(std.testing.allocator, &history, "main");
1742     try std.testing.expect(version.same(main_commit, (try connection.checkout()).head));
1743     try std.testing.expectError(error.TableNotFound, connection.execute(std.testing.allocator, "SELECT title FROM notes WHERE rowid = 1", .{ .durability = .buffered }));
1744     var item = try connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 7", .{ .durability = .buffered });
1745     defer item.deinit(std.testing.allocator);
1746     try std.testing.expectEqual(@as(usize, 1), item.rowCount());
1747     const item_view = try row.View.init(item.nextRow().?);
1748     try std.testing.expectEqualStrings("main", (try item_view.column(0)).text);
1749 
1750     var names_on_main = try connection.catalog.relationNames(std.testing.allocator);
1751     defer names_on_main.deinit();
1752     try std.testing.expectEqual(@as(usize, 1), names_on_main.names.len);
1753     try std.testing.expectEqualStrings("items", names_on_main.names[0]);
1754 
1755     try connection.checkoutBranch(std.testing.allocator, &history, "side");
1756     var found = try connection.execute(std.testing.allocator, "SELECT title FROM notes WHERE rowid = 1", .{ .durability = .buffered });
1757     defer found.deinit(std.testing.allocator);
1758     try std.testing.expectEqual(@as(usize, 1), found.rowCount());
1759     const found_view = try row.View.init(found.nextRow().?);
1760     try std.testing.expectEqualStrings("side", (try found_view.column(0)).text);
1761 }
1762 
1763 test "connection drop table statement keeps pre-drop commits readable" {
1764     var tmp = std.testing.tmpDir(.{});
1765     defer tmp.cleanup();
1766 
1767     var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
1768         .paths = .{ .database = "drop-statement.db", .wal = "drop-statement.wal" },
1769         .header = testingHeader(),
1770     });
1771     defer database.deinit();
1772     try database.reserve(.{ .wal_frames = 960 });
1773 
1774     var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "drop-statement.history", .recovery = .reject });
1775     defer history.deinit();
1776 
1777     var connection = try Connection.create(std.testing.allocator, &database, &history, .{});
1778     defer connection.deinit();
1779     try executeStatement(&connection, "CREATE TABLE items (name)");
1780     try executeStatement(&connection, "INSERT INTO items (rowid, name) VALUES (7, 'kept')");
1781     try connection.stage();
1782     const keep_commit = try connection.commit(&history);
1783     _ = try connection.createBranch(&history, "keep");
1784 
1785     var dropped = try connection.execute(std.testing.allocator, "DROP TABLE items", .{ .durability = .buffered });
1786     defer dropped.deinit(std.testing.allocator);
1787     const drop_flush = switch (dropped) {
1788         .catalog => |flush| flush,
1789         else => return error.UnsupportedStatement,
1790     };
1791     try std.testing.expect(version.same(drop_flush.database, (try connection.workingRoot())));
1792     try std.testing.expect(testFindEntry(connection.session.workingRoot(), "items") == null);
1793     try std.testing.expectError(error.TableNotFound, connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 7", .{ .durability = .buffered }));
1794 
1795     try connection.stage();
1796     const drop_commit = try connection.commit(&history);
1797     var dropped_root = try history.commitDatabaseRoot(std.testing.allocator, drop_commit);
1798     defer dropped_root.deinit();
1799     try std.testing.expectEqual(@as(usize, 0), dropped_root.entries.len);
1800     var kept_root = try history.commitDatabaseRoot(std.testing.allocator, keep_commit);
1801     defer kept_root.deinit();
1802     try std.testing.expectEqual(@as(usize, 1), kept_root.entries.len);
1803     try std.testing.expectEqualStrings("items", kept_root.entries[0].name);
1804 
1805     var names_after_drop = try connection.catalog.relationNames(std.testing.allocator);
1806     defer names_after_drop.deinit();
1807     try std.testing.expectEqual(@as(usize, 0), names_after_drop.names.len);
1808 
1809     try connection.checkoutBranch(std.testing.allocator, &history, "keep");
1810     try std.testing.expect(version.same(keep_commit, (try connection.checkout()).head));
1811     var restored = try connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 7", .{ .durability = .buffered });
1812     defer restored.deinit(std.testing.allocator);
1813     try std.testing.expectEqual(@as(usize, 1), restored.rowCount());
1814     const restored_view = try row.View.init(restored.nextRow().?);
1815     try std.testing.expectEqualStrings("kept", (try restored_view.column(0)).text);
1816 }
1817 
1818 test "connection checkout discards queued relation session edits" {
1819     var tmp = std.testing.tmpDir(.{});
1820     defer tmp.cleanup();
1821 
1822     var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
1823         .paths = .{ .database = "checkout-session-discard.db", .wal = "checkout-session-discard.wal" },
1824         .header = testingHeader(),
1825     });
1826     defer database.deinit();
1827     try database.reserve(.{ .wal_frames = 960 });
1828 
1829     var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "checkout-session-discard.history", .recovery = .reject });
1830     defer history.deinit();
1831 
1832     var connection = try Connection.create(std.testing.allocator, &database, &history, .{});
1833     defer connection.deinit();
1834     try executeStatement(&connection, "CREATE TABLE items (name)");
1835     try connection.stage();
1836     const base_commit = try connection.commit(&history);
1837     _ = try connection.createBranch(&history, "side");
1838 
1839     var prepared = try plan_mod.prepareRelation(&connection.catalog, std.testing.allocator, "items", plan_mod.emptyParameterShape());
1840     var relation_session = try prepared.writeSession();
1841     var relation_session_live = true;
1842     errdefer if (relation_session_live) relation_session.deinit();
1843     try relation_session.put(42, &.{.{ .text = "queued" }});
1844     const write_limits = try relation_session.stagingLimits();
1845     var workspace = try session_mod.DatabaseWrite.Workspace.allocate(
1846         std.testing.allocator,
1847         write_limits,
1848     );
1849     defer workspace.deallocate(std.testing.allocator);
1850     var write = try connection.beginWrite(&workspace, std.testing.allocator, .{
1851         .limits = write_limits,
1852         .durability = .buffered,
1853     });
1854     try write.stageRelation(&relation_session);
1855     relation_session_live = false;
1856     try std.testing.expectEqual(@as(usize, 1), write.pendingRelations());
1857     prepared.deinit();
1858 
1859     try connection.checkoutBranch(std.testing.allocator, &history, "side");
1860     try std.testing.expect(version.same(base_commit, (try connection.checkout()).head));
1861     try std.testing.expectEqual(@as(usize, 0), connection.session.pendingRelations());
1862 
1863     const empty_limits = session_mod.DatabaseWrite.Limits{
1864         .relations = 0,
1865         .edits = 0,
1866         .payload_bytes = 0,
1867         .assignments = 0,
1868     };
1869     var replacement_write = try connection.beginWrite(&workspace, std.testing.allocator, .{
1870         .limits = empty_limits,
1871         .durability = .buffered,
1872     });
1873     defer replacement_write.deinit();
1874     write.deinit();
1875     try std.testing.expectError(
1876         error.WriteSessionActive,
1877         connection.beginWrite(&workspace, std.testing.allocator, .{
1878             .limits = empty_limits,
1879             .durability = .buffered,
1880         }),
1881     );
1882 
1883     var queued = try connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 42", .{ .durability = .buffered });
1884     defer queued.deinit(std.testing.allocator);
1885     try std.testing.expectEqual(@as(usize, 0), queued.rowCount());
1886 }
1887 
1888 test "connection write session stages statements before one root flush" {
1889     var tmp = std.testing.tmpDir(.{});
1890     defer tmp.cleanup();
1891 
1892     var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
1893         .paths = .{ .database = "connection-write-session.db", .wal = "connection-write-session.wal" },
1894         .header = testingHeader(),
1895     });
1896     defer database.deinit();
1897     try database.reserve(.{ .wal_frames = 960 });
1898 
1899     var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "connection-write-session.history", .recovery = .reject });
1900     defer history.deinit();
1901 
1902     var connection = try Connection.create(std.testing.allocator, &database, &history, .{});
1903     defer connection.deinit();
1904     try executeStatement(&connection, "CREATE TABLE items (name)");
1905     const before = (try connection.workingRoot());
1906 
1907     var insert = try connection.prepare(std.testing.allocator, "INSERT INTO items (rowid, name) VALUES (?1, ?2)");
1908     const write_options = WriteOptions{
1909         .limits = .{
1910             .relations = 1,
1911             .edits = 2,
1912             .payload_bytes = 32,
1913             .assignments = 0,
1914         },
1915         .durability = .buffered,
1916     };
1917     var workspace = try session_mod.DatabaseWrite.Workspace.allocate(
1918         std.testing.allocator,
1919         write_options.limits,
1920     );
1921     defer workspace.deallocate(std.testing.allocator);
1922     var write = try connection.beginWrite(
1923         &workspace,
1924         std.testing.allocator,
1925         write_options,
1926     );
1927     defer write.deinit();
1928     try std.testing.expectError(
1929         error.WriteSessionActive,
1930         connection.beginWrite(&workspace, std.testing.allocator, write_options),
1931     );
1932 
1933     try insert.bind(1, .{ .integer = 1 });
1934     try insert.bind(2, .{ .text = "one" });
1935     var first = try connection.executePrepared(&insert, std.testing.allocator, .{ .durability = .buffered, .write = &write });
1936     defer first.deinit(std.testing.allocator);
1937     try std.testing.expectEqual(@as(usize, 1), first.staged);
1938 
1939     try insert.bind(1, .{ .integer = 2 });
1940     try insert.bind(2, .{ .text = "two" });
1941     var second = try connection.executePrepared(&insert, std.testing.allocator, .{ .durability = .buffered, .write = &write });
1942     defer second.deinit(std.testing.allocator);
1943     try std.testing.expectEqual(@as(usize, 1), second.staged);
1944     insert.deinit();
1945     try std.testing.expect(version.same(before, (try connection.workingRoot())));
1946 
1947     var before_flush = try connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid >= 1", .{ .durability = .buffered });
1948     defer before_flush.deinit(std.testing.allocator);
1949     try std.testing.expectEqual(@as(usize, 0), before_flush.rowCount());
1950 
1951     var flush = try write.flush();
1952     defer flush.deinit();
1953     try std.testing.expectEqual(@as(usize, 1), flush.relations.len);
1954     try std.testing.expect(version.same(flush.database, (try connection.workingRoot())));
1955     try std.testing.expect(!version.same(before, (try connection.workingRoot())));
1956     const working_root = connection.session.workingRoot();
1957     try std.testing.expect(version.same(flush.database, working_root.hash));
1958     try std.testing.expectEqual(@as(usize, 1), working_root.entries.len);
1959     try std.testing.expectError(error.DatabaseRootNotFound, history.databaseRoot(std.testing.allocator, flush.database));
1960 
1961     var after_flush = try connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid >= 1", .{ .durability = .buffered });
1962     defer after_flush.deinit(std.testing.allocator);
1963     try std.testing.expectEqual(@as(usize, 2), after_flush.rowCount());
1964 
1965     try connection.stage();
1966     const commit_hash = try connection.commit(&history);
1967     try std.testing.expect(version.same(flush.database, connection.session.workingRoot().hash));
1968     var committed = try history.commitDatabaseRoot(std.testing.allocator, commit_hash);
1969     defer committed.deinit();
1970     try std.testing.expect(version.same(flush.database, committed.hash));
1971     const rows = try history.relationRows(std.testing.allocator, flush.relations[0].relation);
1972     defer version.freeRelationRows(std.testing.allocator, rows);
1973     try std.testing.expectEqual(@as(usize, 2), rows.len);
1974 }
1975 
1976 test "connection diffs relation views without raw catalog access" {
1977     var tmp = std.testing.tmpDir(.{});
1978     defer tmp.cleanup();
1979 
1980     var left = TestingConnection{};
1981     try left.init(std.testing.allocator, tmp.dir, "left.db", "left.wal", "left.history");
1982     defer left.deinit();
1983     var right = TestingConnection{};
1984     try right.init(std.testing.allocator, tmp.dir, "right.db", "right.wal", "right.history");
1985     defer right.deinit();
1986 
1987     try executeStatement(&left.connection, "CREATE TABLE items (name)");
1988     try executeStatement(&left.connection, "INSERT INTO items (rowid, name) VALUES (1, 'same')");
1989     try executeStatement(&left.connection, "INSERT INTO items (rowid, name) VALUES (2, 'old')");
1990 
1991     try executeStatement(&right.connection, "CREATE TABLE items (name)");
1992     try executeStatement(&right.connection, "INSERT INTO items (rowid, name) VALUES (1, 'same')");
1993     try executeStatement(&right.connection, "INSERT INTO items (rowid, name) VALUES (2, 'new')");
1994     try executeStatement(&right.connection, "INSERT INTO items (rowid, name) VALUES (3, 'added')");
1995 
1996     var right_view = try right.connection.relationView(std.testing.allocator, "items");
1997     defer right_view.deinit();
1998     var result = try left.connection.diffRelation(std.testing.allocator, "items", right_view.snapshot());
1999     defer result.deinit();
2000 
2001     try std.testing.expect(!result.schema_changed);
2002     try std.testing.expectEqual(@as(usize, 2), result.changes.len);
2003     try std.testing.expectEqual(diff_mod.ChangeKind.modified, result.changes[0].kind);
2004     try std.testing.expectEqual(@as(i64, 2), result.changes[0].rowid);
2005     try std.testing.expectEqual(diff_mod.ChangeKind.added, result.changes[1].kind);
2006     try std.testing.expectEqual(@as(i64, 3), result.changes[1].rowid);
2007 }
2008 
2009 test "connection applies explicit database merge snapshots into working root" {
2010     var tmp = std.testing.tmpDir(.{});
2011     defer tmp.cleanup();
2012 
2013     var base = TestingConnection{};
2014     try base.init(std.testing.allocator, tmp.dir, "merge-base.db", "merge-base.wal", "merge-base.history");
2015     defer base.deinit();
2016     var ours = TestingConnection{};
2017     try ours.init(std.testing.allocator, tmp.dir, "merge-ours.db", "merge-ours.wal", "merge-ours.history");
2018     defer ours.deinit();
2019     var theirs = TestingConnection{};
2020     try theirs.init(std.testing.allocator, tmp.dir, "merge-theirs.db", "merge-theirs.wal", "merge-theirs.history");
2021     defer theirs.deinit();
2022 
2023     try createItems(&base.connection);
2024     try executeStatement(&base.connection, "INSERT INTO items (rowid, name) VALUES (1, 'same')");
2025     try executeStatement(&base.connection, "INSERT INTO items (rowid, name) VALUES (2, 'base')");
2026     try executeStatement(&base.connection, "INSERT INTO items (rowid, name) VALUES (3, 'delete')");
2027 
2028     try createItems(&ours.connection);
2029     try executeStatement(&ours.connection, "INSERT INTO items (rowid, name) VALUES (1, 'same')");
2030     try executeStatement(&ours.connection, "INSERT INTO items (rowid, name) VALUES (2, 'ours')");
2031     try executeStatement(&ours.connection, "INSERT INTO items (rowid, name) VALUES (3, 'delete')");
2032     try executeStatement(&ours.connection, "INSERT INTO items (rowid, name) VALUES (5, 'ours-add')");
2033     try executeStatement(&ours.connection, "ANALYZE items");
2034     var ours_stats = (try ours.connection.catalog.relationStats(std.testing.allocator, "items")).?;
2035     ours_stats.deinit();
2036 
2037     try createItems(&theirs.connection);
2038     try executeStatement(&theirs.connection, "INSERT INTO items (rowid, name) VALUES (1, 'same')");
2039     try executeStatement(&theirs.connection, "INSERT INTO items (rowid, name) VALUES (2, 'base')");
2040     try executeStatement(&theirs.connection, "INSERT INTO items (rowid, name) VALUES (6, 'theirs-add')");
2041 
2042     const names = [_][]const u8{"items"};
2043     var base_view = try base.connection.databaseView(std.testing.allocator, &base.history, names[0..]);
2044     defer base_view.deinit();
2045     var theirs_view = try theirs.connection.databaseView(std.testing.allocator, &theirs.history, names[0..]);
2046     defer theirs_view.deinit();
2047     const previous_root = (try ours.connection.workingRoot());
2048 
2049     var merged = try ours.connection.mergeDatabase(std.testing.allocator, &ours.history, base_view.snapshot(), theirs_view.snapshot(), .{});
2050     defer merged.deinit();
2051     try std.testing.expect(!merged.hasConflicts());
2052     try std.testing.expect(!version.same(previous_root, (try ours.connection.workingRoot())));
2053     try std.testing.expect(version.same(merged.value.root.hash, (try ours.connection.workingRoot())));
2054     try std.testing.expect(version.same(merged.value.root.hash, ours.connection.session.workingRoot().hash));
2055     try ours.history.validateConflictRoot(merged.value.root.conflicts);
2056     try expectLiveDatabaseRoot(&ours.connection, &merged.value.root);
2057     const merged_relation = merged.value.findRelation("items").?;
2058     try std.testing.expect(version.same(merged_relation.root.stats.hash, version.emptyHash("sql.stats.none")));
2059     try std.testing.expect((try ours.connection.catalog.relationStats(std.testing.allocator, "items")) == null);
2060     try std.testing.expect((try ours.connection.checkout()).working.dirty());
2061     try std.testing.expectError(error.DatabaseRootNotFound, ours.history.databaseRoot(std.testing.allocator, merged.value.root.hash));
2062 
2063     var found = try ours.connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 6", .{ .durability = .buffered });
2064     defer found.deinit(std.testing.allocator);
2065     try std.testing.expectEqual(@as(usize, 1), found.rowCount());
2066     const added_view = try row.View.init(found.nextRow().?);
2067     try std.testing.expectEqualStrings("theirs-add", (try added_view.column(0)).text);
2068 
2069     var missing = try ours.connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 3", .{ .durability = .buffered });
2070     defer missing.deinit(std.testing.allocator);
2071     try std.testing.expectEqual(@as(usize, 0), missing.rowCount());
2072 
2073     try ours.connection.stage();
2074     const merge_commit = try ours.connection.commit(&ours.history);
2075     var committed_value = try ours.history.databaseValue(std.testing.allocator, (try ours.connection.workingRoot()));
2076     defer committed_value.deinit();
2077     try std.testing.expect(version.same(merge_commit, (try ours.connection.checkout()).head));
2078     try std.testing.expect(version.same(merged.value.root.hash, committed_value.root.hash));
2079 }
2080 
2081 test "connection rejects a selected schema before changing the working root" {
2082     var tmp = std.testing.tmpDir(.{});
2083     defer tmp.cleanup();
2084 
2085     var base = TestingConnection{};
2086     try base.init(std.testing.allocator, tmp.dir, "schema-base.db", "schema-base.wal", "schema-base.history");
2087     defer base.deinit();
2088     var ours = TestingConnection{};
2089     try ours.init(std.testing.allocator, tmp.dir, "schema-ours.db", "schema-ours.wal", "schema-ours.history");
2090     defer ours.deinit();
2091     var theirs = TestingConnection{};
2092     try theirs.init(std.testing.allocator, tmp.dir, "schema-theirs.db", "schema-theirs.wal", "schema-theirs.history");
2093     defer theirs.deinit();
2094 
2095     try createItems(&base.connection);
2096     try createItems(&ours.connection);
2097     try createItems(&theirs.connection);
2098     try executeStatement(&base.connection, "CREATE TABLE alpha (name)");
2099     try executeStatement(&ours.connection, "CREATE TABLE alpha (name)");
2100     try executeStatement(&theirs.connection, "CREATE TABLE alpha (name)");
2101     try executeStatement(&base.connection, "INSERT INTO alpha (rowid, name) VALUES (1, 'base')");
2102     try executeStatement(&ours.connection, "INSERT INTO alpha (rowid, name) VALUES (1, 'base')");
2103     try executeStatement(&theirs.connection, "INSERT INTO alpha (rowid, name) VALUES (1, 'theirs')");
2104     try executeStatement(&theirs.connection, "CREATE INDEX items_name ON items (name)");
2105 
2106     const names = [_][]const u8{ "alpha", "items" };
2107     var base_view = try base.connection.databaseView(std.testing.allocator, &base.history, names[0..]);
2108     defer base_view.deinit();
2109     var theirs_view = try theirs.connection.databaseView(std.testing.allocator, &theirs.history, names[0..]);
2110     defer theirs_view.deinit();
2111     const previous_root = (try ours.connection.workingRoot());
2112 
2113     try std.testing.expectError(error.UnsupportedCheckoutRoot, ours.connection.mergeDatabase(
2114         std.testing.allocator,
2115         &ours.history,
2116         base_view.snapshot(),
2117         theirs_view.snapshot(),
2118         .{},
2119     ));
2120     try std.testing.expect(version.same(previous_root, (try ours.connection.workingRoot())));
2121     var items = try ours.connection.relationView(std.testing.allocator, "items");
2122     defer items.deinit();
2123     try std.testing.expectEqual(@as(usize, 0), items.root.indexes.len);
2124     var alpha = try ours.connection.execute(std.testing.allocator, "SELECT name FROM alpha WHERE rowid = 1", .{ .durability = .buffered });
2125     defer alpha.deinit(std.testing.allocator);
2126     try std.testing.expectEqual(@as(usize, 1), alpha.rowCount());
2127     const alpha_view = try row.View.init(alpha.nextRow().?);
2128     try std.testing.expectEqualStrings("base", (try alpha_view.column(0)).text);
2129 }
2130 
2131 test "connection replaces conflict root after value resolution" {
2132     var tmp = std.testing.tmpDir(.{});
2133     defer tmp.cleanup();
2134 
2135     var base = TestingConnection{};
2136     try base.init(std.testing.allocator, tmp.dir, "resolve-base.db", "resolve-base.wal", "resolve-base.history");
2137     defer base.deinit();
2138     var ours = TestingConnection{};
2139     try ours.init(std.testing.allocator, tmp.dir, "resolve-ours.db", "resolve-ours.wal", "resolve-ours.history");
2140     defer ours.deinit();
2141     var theirs = TestingConnection{};
2142     try theirs.init(std.testing.allocator, tmp.dir, "resolve-theirs.db", "resolve-theirs.wal", "resolve-theirs.history");
2143     defer theirs.deinit();
2144 
2145     try createItems(&base.connection);
2146     try executeStatement(&base.connection, "INSERT INTO items (rowid, name) VALUES (1, 'base')");
2147 
2148     try createItems(&ours.connection);
2149     try executeStatement(&ours.connection, "INSERT INTO items (rowid, name) VALUES (1, 'ours')");
2150 
2151     try createItems(&theirs.connection);
2152     try executeStatement(&theirs.connection, "INSERT INTO items (rowid, name) VALUES (1, 'theirs')");
2153 
2154     const names = [_][]const u8{"items"};
2155     var base_view = try base.connection.databaseView(std.testing.allocator, &base.history, names[0..]);
2156     defer base_view.deinit();
2157     var theirs_view = try theirs.connection.databaseView(std.testing.allocator, &theirs.history, names[0..]);
2158     defer theirs_view.deinit();
2159 
2160     var merged = try ours.connection.mergeDatabase(std.testing.allocator, &ours.history, base_view.snapshot(), theirs_view.snapshot(), .{});
2161     defer merged.deinit();
2162     try std.testing.expect(merged.hasConflicts());
2163     try std.testing.expectEqual(@as(usize, 1), merged.conflict_root.count);
2164     try std.testing.expect(version.same(merged.conflict_root.hash, ours.connection.session.workingRoot().conflicts));
2165     try ours.history.validateConflictRoot(merged.value.root.conflicts);
2166     try expectLiveDatabaseRoot(&ours.connection, &merged.value.root);
2167     var neutral_root = try version.databaseRoot(
2168         std.testing.allocator,
2169         &ours.connection.catalog,
2170         version.ConflictRoot.empty().hash,
2171     );
2172     defer neutral_root.deinit();
2173     try std.testing.expect(!version.same(neutral_root.hash, merged.value.root.hash));
2174     var persisted_entries = try ours.history.conflictEntries(std.testing.allocator, merged.conflict_root.hash);
2175     defer persisted_entries.deinit();
2176     try std.testing.expectEqual(@as(usize, 1), persisted_entries.entries.len);
2177 
2178     const conflicted_root = (try ours.connection.workingRoot());
2179     const clean_root = try ours.connection.replaceConflictRoot(version.ConflictRoot.empty());
2180     try std.testing.expect(!version.same(conflicted_root, clean_root));
2181     try std.testing.expect(version.same(version.ConflictRoot.empty().hash, ours.connection.session.workingRoot().conflicts));
2182 
2183     var selected = try ours.connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 1", .{ .durability = .buffered });
2184     defer selected.deinit(std.testing.allocator);
2185     try std.testing.expectEqual(@as(usize, 1), selected.rowCount());
2186     const selected_view = try row.View.init(selected.nextRow().?);
2187     try std.testing.expectEqualStrings("ours", (try selected_view.column(0)).text);
2188 
2189     try ours.connection.stage();
2190     const commit_hash = try ours.connection.commit(&ours.history);
2191     try std.testing.expect(version.same(commit_hash, (try ours.connection.checkout()).head));
2192     var committed = try ours.history.databaseValue(std.testing.allocator, clean_root);
2193     defer committed.deinit();
2194     try std.testing.expect(version.same(clean_root, committed.root.hash));
2195     try std.testing.expect(version.same(version.ConflictRoot.empty().hash, committed.root.conflicts));
2196 }
2197 
2198 test "connection merge materialization rejects mismatched and unclosed conflicts" {
2199     var tmp = std.testing.tmpDir(.{});
2200     defer tmp.cleanup();
2201 
2202     var store = TestingConnection{};
2203     try store.init(
2204         std.testing.allocator,
2205         tmp.dir,
2206         "merge-conflict-closure.db",
2207         "merge-conflict-closure.wal",
2208         "merge-conflict-closure.history",
2209     );
2210     defer store.deinit();
2211 
2212     try createItems(&store.connection);
2213     try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'ours')");
2214     const baseline = (try store.connection.workingRoot());
2215     const artifact = version.ConflictArtifact.init("items", 1, "base", "ours", "theirs");
2216     const conflicts = version.ConflictRoot.init(&.{artifact.entry()});
2217     var value = try store.connection.materializedWorkingValue(std.testing.allocator);
2218     const rooted = try value.root.withConflicts(std.testing.allocator, conflicts.hash);
2219     value.root.deinit();
2220     value.root = rooted;
2221     var artifacts = [_]version.ConflictArtifact{artifact};
2222     var result = merge_mod.DatabaseMerge{
2223         .allocator = std.testing.allocator,
2224         .value = value,
2225         .conflict_root = version.ConflictRoot.empty(),
2226         .relations = &.{},
2227         .discovered = &.{},
2228         .artifacts = artifacts[0..],
2229     };
2230     defer result.value.deinit();
2231 
2232     try std.testing.expectError(
2233         error.InvalidHistory,
2234         store.connection.materializeDatabaseMerge(
2235             std.testing.allocator,
2236             &store.history,
2237             &result,
2238             .{ .durability = .buffered },
2239         ),
2240     );
2241     try std.testing.expect(version.same(baseline, (try store.connection.workingRoot())));
2242 
2243     result.conflict_root = conflicts;
2244     try store.history.putConflict(artifact);
2245     store.history.findConflict(artifact.hash).?.artifact.rowid += 1;
2246     try std.testing.expectError(
2247         error.InvalidHistory,
2248         store.connection.materializeDatabaseMerge(
2249             std.testing.allocator,
2250             &store.history,
2251             &result,
2252             .{ .durability = .buffered },
2253         ),
2254     );
2255     try std.testing.expect(version.same(baseline, (try store.connection.workingRoot())));
2256 }
2257 
2258 test "connection materialized root verifier rejects relation identity drift" {
2259     var tmp = std.testing.tmpDir(.{});
2260     defer tmp.cleanup();
2261 
2262     var store = TestingConnection{};
2263     try store.init(std.testing.allocator, tmp.dir, "merge-relation-drift.db", "merge-relation-drift.wal", "merge-relation-drift.history");
2264     defer store.deinit();
2265 
2266     try createItems(&store.connection);
2267     try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'target')");
2268     var target = try store.connection.materializedWorkingValue(std.testing.allocator);
2269     defer target.deinit();
2270     try executeStatement(&store.connection, "UPDATE items SET name = 'drift' WHERE rowid = 1");
2271 
2272     try std.testing.expectError(
2273         error.InvalidHistory,
2274         store.connection.verifyMaterializedDatabaseRoot(std.testing.allocator, &target.root),
2275     );
2276     var live = try version.databaseRoot(
2277         std.testing.allocator,
2278         &store.connection.catalog,
2279         target.root.conflicts,
2280     );
2281     defer live.deinit();
2282     try std.testing.expect(!version.same(live.hash, target.root.hash));
2283 }
2284 
2285 test "connection materialized root verifier rejects an extra live relation" {
2286     var tmp = std.testing.tmpDir(.{});
2287     defer tmp.cleanup();
2288 
2289     var store = TestingConnection{};
2290     try store.init(std.testing.allocator, tmp.dir, "merge-extra-relation.db", "merge-extra-relation.wal", "merge-extra-relation.history");
2291     defer store.deinit();
2292 
2293     try createItems(&store.connection);
2294     try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'target')");
2295     var target = try store.connection.materializedWorkingValue(std.testing.allocator);
2296     defer target.deinit();
2297     try executeStatement(&store.connection, "CREATE TABLE untracked (value)");
2298 
2299     const entry = relationEntry(target.root.entries, "items").?;
2300     var handle = try store.connection.catalog.openRelation(std.testing.allocator, "items");
2301     defer handle.deinit();
2302     var stats = try store.connection.catalog.relationStats(std.testing.allocator, "items");
2303     defer if (stats) |*relation_stats| relation_stats.deinit();
2304     const live = try version.relationKey(
2305         "items",
2306         &handle,
2307         if (stats) |*relation_stats| relation_stats else null,
2308     );
2309     try std.testing.expect(version.same(live.hash, entry.hash));
2310     try std.testing.expectError(
2311         error.InvalidHistory,
2312         store.connection.verifyMaterializedDatabaseRoot(std.testing.allocator, &target.root),
2313     );
2314 }
2315 
2316 test "connection resolves selected conflict artifacts" {
2317     var tmp = std.testing.tmpDir(.{});
2318     defer tmp.cleanup();
2319 
2320     var base = TestingConnection{};
2321     try base.init(std.testing.allocator, tmp.dir, "resolve-selected-base.db", "resolve-selected-base.wal", "resolve-selected-base.history");
2322     defer base.deinit();
2323     var ours = TestingConnection{};
2324     try ours.init(std.testing.allocator, tmp.dir, "resolve-selected-ours.db", "resolve-selected-ours.wal", "resolve-selected-ours.history");
2325     defer ours.deinit();
2326     var theirs = TestingConnection{};
2327     try theirs.init(std.testing.allocator, tmp.dir, "resolve-selected-theirs.db", "resolve-selected-theirs.wal", "resolve-selected-theirs.history");
2328     defer theirs.deinit();
2329 
2330     try createItems(&base.connection);
2331     try executeStatement(&base.connection, "INSERT INTO items (rowid, name) VALUES (1, 'base-one')");
2332     try executeStatement(&base.connection, "INSERT INTO items (rowid, name) VALUES (2, 'base-two')");
2333 
2334     try createItems(&ours.connection);
2335     try executeStatement(&ours.connection, "INSERT INTO items (rowid, name) VALUES (1, 'ours-one')");
2336     try executeStatement(&ours.connection, "INSERT INTO items (rowid, name) VALUES (2, 'ours-two')");
2337 
2338     try createItems(&theirs.connection);
2339     try executeStatement(&theirs.connection, "INSERT INTO items (rowid, name) VALUES (1, 'theirs-one')");
2340     try executeStatement(&theirs.connection, "INSERT INTO items (rowid, name) VALUES (2, 'theirs-two')");
2341 
2342     const names = [_][]const u8{"items"};
2343     var base_view = try base.connection.databaseView(std.testing.allocator, &base.history, names[0..]);
2344     defer base_view.deinit();
2345     var theirs_view = try theirs.connection.databaseView(std.testing.allocator, &theirs.history, names[0..]);
2346     defer theirs_view.deinit();
2347 
2348     var merged = try ours.connection.mergeDatabase(std.testing.allocator, &ours.history, base_view.snapshot(), theirs_view.snapshot(), .{});
2349     defer merged.deinit();
2350     try std.testing.expect(merged.hasConflicts());
2351     try std.testing.expectEqual(@as(usize, 2), merged.conflict_root.count);
2352 
2353     var artifacts = try ours.connection.conflictArtifacts(std.testing.allocator, &ours.history);
2354     defer artifacts.deinit();
2355     try std.testing.expect(version.same(merged.conflict_root.hash, artifacts.root.hash));
2356     try std.testing.expectEqual(@as(usize, 2), artifacts.artifacts.len);
2357     try std.testing.expectEqual(@as(i64, 1), artifacts.artifacts[0].rowid);
2358     try std.testing.expect(artifacts.artifacts[0].base != null);
2359     try std.testing.expect(artifacts.artifacts[0].ours != null);
2360     try std.testing.expect(artifacts.artifacts[0].theirs != null);
2361     try std.testing.expectEqual(@as(i64, 2), artifacts.artifacts[1].rowid);
2362     try std.testing.expect(artifacts.artifacts[1].base != null);
2363     try std.testing.expect(artifacts.artifacts[1].ours != null);
2364     try std.testing.expect(artifacts.artifacts[1].theirs != null);
2365 
2366     const conflicted_root = (try ours.connection.workingRoot());
2367     const unchanged_root = try ours.connection.resolveConflicts(std.testing.allocator, &ours.history, &.{});
2368     try std.testing.expect(version.same(conflicted_root, unchanged_root));
2369 
2370     const missing = version.emptyHash("sql.conflict.missing");
2371     try std.testing.expectError(error.ConflictNotFound, ours.connection.resolveConflicts(std.testing.allocator, &ours.history, &.{missing}));
2372     try std.testing.expect(version.same(conflicted_root, (try ours.connection.workingRoot())));
2373 
2374     const first_hash = artifacts.artifacts[0].hash;
2375     const remaining_hash = artifacts.artifacts[1].hash;
2376     const partially_clean_root = try ours.connection.resolveConflicts(std.testing.allocator, &ours.history, &.{first_hash});
2377     try std.testing.expect(!version.same(conflicted_root, partially_clean_root));
2378 
2379     var remaining_artifacts = try ours.connection.conflictArtifacts(std.testing.allocator, &ours.history);
2380     defer remaining_artifacts.deinit();
2381     try std.testing.expectEqual(@as(usize, 1), remaining_artifacts.artifacts.len);
2382     try std.testing.expectEqual(@as(i64, 2), remaining_artifacts.artifacts[0].rowid);
2383     try std.testing.expect(version.same(remaining_hash, remaining_artifacts.artifacts[0].hash));
2384 
2385     const clean_root = try ours.connection.resolveConflicts(std.testing.allocator, &ours.history, &.{remaining_hash});
2386     try std.testing.expect(!version.same(partially_clean_root, clean_root));
2387     try std.testing.expect(version.same(version.ConflictRoot.empty().hash, ours.connection.session.workingRoot().conflicts));
2388 
2389     var selected_one = try ours.connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 1", .{ .durability = .buffered });
2390     defer selected_one.deinit(std.testing.allocator);
2391     try std.testing.expectEqual(@as(usize, 1), selected_one.rowCount());
2392     const selected_one_view = try row.View.init(selected_one.nextRow().?);
2393     try std.testing.expectEqualStrings("ours-one", (try selected_one_view.column(0)).text);
2394 
2395     var selected_two = try ours.connection.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 2", .{ .durability = .buffered });
2396     defer selected_two.deinit(std.testing.allocator);
2397     try std.testing.expectEqual(@as(usize, 1), selected_two.rowCount());
2398     const selected_two_view = try row.View.init(selected_two.nextRow().?);
2399     try std.testing.expectEqualStrings("ours-two", (try selected_two_view.column(0)).text);
2400 
2401     try ours.connection.stage();
2402     const commit_hash = try ours.connection.commit(&ours.history);
2403     try std.testing.expect(version.same(commit_hash, (try ours.connection.checkout()).head));
2404     var committed = try ours.history.databaseValue(std.testing.allocator, clean_root);
2405     defer committed.deinit();
2406     try std.testing.expect(version.same(clean_root, committed.root.hash));
2407     try std.testing.expect(version.same(version.ConflictRoot.empty().hash, committed.root.conflicts));
2408 }
2409 
2410 test "connection merge commit records parent when resolved root matches ours" {
2411     var tmp = std.testing.tmpDir(.{});
2412     defer tmp.cleanup();
2413 
2414     var store = TestingConnection{};
2415     try store.init(std.testing.allocator, tmp.dir, "merge-same-root.db", "merge-same-root.wal", "merge-same-root.history");
2416     defer store.deinit();
2417 
2418     try createItems(&store.connection);
2419     try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'base')");
2420     try store.connection.stage();
2421     _ = try store.connection.commit(&store.history);
2422 
2423     _ = try store.connection.createBranch(&store.history, "side");
2424     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "side");
2425     try executeStatement(&store.connection, "DELETE FROM items WHERE rowid = 1");
2426     try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'theirs')");
2427     try store.connection.stage();
2428     const side_commit = try store.connection.commit(&store.history);
2429 
2430     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main");
2431     try executeStatement(&store.connection, "DELETE FROM items WHERE rowid = 1");
2432     try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'ours')");
2433     try store.connection.stage();
2434     const ours_commit = try store.connection.commit(&store.history);
2435     const ours_root = (try store.connection.workingRoot());
2436 
2437     var merged = try store.connection.mergeBranch(std.testing.allocator, &store.history, "side", .{});
2438     defer merged.deinit();
2439     try std.testing.expect(merged.hasConflicts());
2440     var artifacts = try store.connection.conflictArtifacts(std.testing.allocator, &store.history);
2441     defer artifacts.deinit();
2442     try std.testing.expectEqual(@as(usize, 1), artifacts.artifacts.len);
2443     _ = try store.connection.resolveConflicts(std.testing.allocator, &store.history, &.{artifacts.artifacts[0].hash});
2444     try std.testing.expect(version.same(ours_root, (try store.connection.workingRoot())));
2445 
2446     const merge_commit = try store.connection.mergeCommit(&store.history, side_commit);
2447     const commit = try store.history.commitValue(merge_commit);
2448     try std.testing.expect(version.same(ours_root, commit.root));
2449     try std.testing.expectEqual(@as(usize, 2), commit.parents.len);
2450     try std.testing.expect(version.same(ours_commit, commit.parents[0]));
2451     try std.testing.expect(version.same(side_commit, commit.parents[1]));
2452 }
2453 
2454 test "connection merge materializes independent relation additions" {
2455     var tmp = std.testing.tmpDir(.{});
2456     defer tmp.cleanup();
2457 
2458     var base = TestingConnection{};
2459     try base.init(std.testing.allocator, tmp.dir, "merge-add-base.db", "merge-add-base.wal", "merge-add-base.history");
2460     defer base.deinit();
2461     var ours = TestingConnection{};
2462     try ours.init(std.testing.allocator, tmp.dir, "merge-add-ours.db", "merge-add-ours.wal", "merge-add-ours.history");
2463     defer ours.deinit();
2464     var theirs = TestingConnection{};
2465     try theirs.init(std.testing.allocator, tmp.dir, "merge-add-theirs.db", "merge-add-theirs.wal", "merge-add-theirs.history");
2466     defer theirs.deinit();
2467 
2468     try createItems(&base.connection);
2469     try executeStatement(&base.connection, "INSERT INTO items (rowid, name) VALUES (1, 'base')");
2470 
2471     try createItems(&ours.connection);
2472     try executeStatement(&ours.connection, "INSERT INTO items (rowid, name) VALUES (1, 'base')");
2473     try executeStatement(&ours.connection, "CREATE TABLE local (body)");
2474     try executeStatement(&ours.connection, "INSERT INTO local (rowid, body) VALUES (7, 'ours-only')");
2475 
2476     try createItems(&theirs.connection);
2477     try executeStatement(&theirs.connection, "INSERT INTO items (rowid, name) VALUES (1, 'base')");
2478     try executeStatement(&theirs.connection, "CREATE TABLE side (body)");
2479     try executeStatement(&theirs.connection, "INSERT INTO side (rowid, body) VALUES (8, 'theirs-only')");
2480 
2481     const base_names = [_][]const u8{"items"};
2482     const theirs_names = [_][]const u8{ "side", "items" };
2483     var base_view = try base.connection.databaseView(std.testing.allocator, &base.history, base_names[0..]);
2484     defer base_view.deinit();
2485     var theirs_view = try theirs.connection.databaseView(std.testing.allocator, &theirs.history, theirs_names[0..]);
2486     defer theirs_view.deinit();
2487 
2488     var merged = try ours.connection.mergeDatabase(std.testing.allocator, &ours.history, base_view.snapshot(), theirs_view.snapshot(), .{});
2489     defer merged.deinit();
2490     try std.testing.expect(!merged.hasConflicts());
2491     try std.testing.expect(merged.value.findRelation("items") != null);
2492     try std.testing.expect(merged.value.findRelation("local") != null);
2493     try std.testing.expect(merged.value.findRelation("side") != null);
2494     try std.testing.expect(version.same(merged.value.root.hash, ours.connection.session.workingRoot().hash));
2495 
2496     var local = try ours.connection.execute(std.testing.allocator, "SELECT body FROM local WHERE rowid = 7", .{ .durability = .buffered });
2497     defer local.deinit(std.testing.allocator);
2498     try std.testing.expectEqual(@as(usize, 1), local.rowCount());
2499     const local_view = try row.View.init(local.nextRow().?);
2500     try std.testing.expectEqualStrings("ours-only", (try local_view.column(0)).text);
2501 
2502     var side = try ours.connection.execute(std.testing.allocator, "SELECT body FROM side WHERE rowid = 8", .{ .durability = .buffered });
2503     defer side.deinit(std.testing.allocator);
2504     try std.testing.expectEqual(@as(usize, 1), side.rowCount());
2505     const side_view = try row.View.init(side.nextRow().?);
2506     try std.testing.expectEqualStrings("theirs-only", (try side_view.column(0)).text);
2507 }
2508 
2509 test "connection merges branch refs from history snapshots" {
2510     var tmp = std.testing.tmpDir(.{});
2511     defer tmp.cleanup();
2512 
2513     var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
2514         .paths = .{ .database = "merge-branch.db", .wal = "merge-branch.wal" },
2515         .header = testingHeader(),
2516     });
2517     defer database.deinit();
2518     try database.reserve(.{ .wal_frames = 960 });
2519 
2520     var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "merge-branch.history", .recovery = .reject });
2521     defer history.deinit();
2522 
2523     var main = try Connection.create(std.testing.allocator, &database, &history, .{});
2524     defer main.deinit();
2525     try createItems(&main);
2526     try executeStatement(&main, "INSERT INTO items (rowid, name) VALUES (1, 'base')");
2527     try main.stage();
2528     const base_commit = try main.commit(&history);
2529     _ = try main.createBranch(&history, "side");
2530 
2531     try executeStatement(&main, "INSERT INTO items (rowid, name) VALUES (2, 'main')");
2532     try main.stage();
2533     const main_commit = try main.commit(&history);
2534 
2535     try main.checkoutBranch(std.testing.allocator, &history, "side");
2536     var main_row_on_side = try main.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 2", .{ .durability = .buffered });
2537     defer main_row_on_side.deinit(std.testing.allocator);
2538     try std.testing.expectEqual(@as(usize, 0), main_row_on_side.rowCount());
2539     try executeStatement(&main, "INSERT INTO items (rowid, name) VALUES (3, 'side')");
2540     try main.stage();
2541     const side_commit = try main.commit(&history);
2542 
2543     try std.testing.expect(version.same(side_commit, (try history.ref("side")).?.target));
2544 
2545     try main.checkoutBranch(std.testing.allocator, &history, "main");
2546     try std.testing.expect(version.same(main_commit, (try main.checkout()).head));
2547     try std.testing.expect(version.same(base_commit, (try main.mergeBase(std.testing.allocator, &history, "side")).?));
2548     var side_row_on_main = try main.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 3", .{ .durability = .buffered });
2549     defer side_row_on_main.deinit(std.testing.allocator);
2550     try std.testing.expectEqual(@as(usize, 0), side_row_on_main.rowCount());
2551     var main_row_on_main = try main.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 2", .{ .durability = .buffered });
2552     defer main_row_on_main.deinit(std.testing.allocator);
2553     try std.testing.expectEqual(@as(usize, 1), main_row_on_main.rowCount());
2554 
2555     const previous_root = (try main.workingRoot());
2556     var merged = try main.mergeBranch(std.testing.allocator, &history, "side", .{});
2557     defer merged.deinit();
2558     try std.testing.expect(!merged.hasConflicts());
2559     try std.testing.expect(!version.same(previous_root, (try main.workingRoot())));
2560     try std.testing.expect(version.same(merged.value.root.hash, (try main.workingRoot())));
2561     try std.testing.expect(version.same(merged.value.root.hash, main.session.workingRoot().hash));
2562 
2563     var found = try main.execute(std.testing.allocator, "SELECT name FROM items WHERE rowid = 3", .{ .durability = .buffered });
2564     defer found.deinit(std.testing.allocator);
2565     try std.testing.expectEqual(@as(usize, 1), found.rowCount());
2566     const found_view = try row.View.init(found.nextRow().?);
2567     try std.testing.expectEqualStrings("side", (try found_view.column(0)).text);
2568 }
2569 
2570 test "committed conflict survives identical later merge" {
2571     var tmp = std.testing.tmpDir(.{});
2572     defer tmp.cleanup();
2573 
2574     var store = TestingConnection{};
2575     try store.init(std.testing.allocator, tmp.dir, "conflict-carry.db", "conflict-carry.wal", "conflict-carry.history");
2576     defer store.deinit();
2577 
2578     const conflict = try commitTestingConflict(&store);
2579     try std.testing.expect(version.same(conflict.commit, (try store.connection.checkout()).head));
2580     _ = try store.connection.createBranch(&store.history, "same");
2581 
2582     var later = try store.connection.mergeBranch(std.testing.allocator, &store.history, "same", .{});
2583     defer later.deinit();
2584     try std.testing.expect(later.hasConflicts());
2585     try std.testing.expectEqual(@as(usize, 1), later.conflict_root.count);
2586     try std.testing.expect(version.same(conflict.root, later.conflict_root.hash));
2587     try std.testing.expect(version.same(conflict.database, (try store.connection.workingRoot())));
2588     try expectTestingConflict(&store, conflict);
2589 
2590     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "side");
2591     try std.testing.expect(version.same(version.ConflictRoot.empty().hash, store.connection.session.workingRoot().conflicts));
2592     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main");
2593     try std.testing.expect(version.same(conflict.commit, (try store.connection.checkout()).head));
2594     try std.testing.expect(version.same(conflict.database, (try store.connection.workingRoot())));
2595     try expectTestingConflict(&store, conflict);
2596 
2597     try executeStatement(&store.connection, "ANALYZE items");
2598     try executeStatement(&store.connection, "CREATE TABLE notes (value)");
2599     try executeStatement(&store.connection, "DROP TABLE notes");
2600     try expectTestingConflict(&store, conflict);
2601 
2602     _ = try store.connection.resolveConflicts(std.testing.allocator, &store.history, &.{conflict.artifact});
2603     try std.testing.expect(version.same(version.ConflictRoot.empty().hash, store.connection.session.workingRoot().conflicts));
2604 }
2605 
2606 test "connection fast forward adopts a committed conflict root" {
2607     var tmp = std.testing.tmpDir(.{});
2608     defer tmp.cleanup();
2609 
2610     var store = TestingConnection{};
2611     try store.init(std.testing.allocator, tmp.dir, "conflict-forward.db", "conflict-forward.wal", "conflict-forward.history");
2612     defer store.deinit();
2613 
2614     const conflict = try commitTestingConflict(&store);
2615     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "behind");
2616     try store.connection.fastForwardBranch(std.testing.allocator, &store.history, conflict.commit);
2617     try std.testing.expect(version.same(conflict.commit, (try store.connection.checkout()).head));
2618     try std.testing.expect(version.same(conflict.commit, (try store.history.ref("behind")).?.target));
2619     try std.testing.expect(version.same(conflict.database, (try store.connection.workingRoot())));
2620     try expectTestingConflict(&store, conflict);
2621 }
2622 
2623 test "connection commit rejects an unclosed conflict root" {
2624     var tmp = std.testing.tmpDir(.{});
2625     defer tmp.cleanup();
2626 
2627     var store = TestingConnection{};
2628     try store.init(
2629         std.testing.allocator,
2630         tmp.dir,
2631         "conflict-closure.db",
2632         "conflict-closure.wal",
2633         "conflict-closure.history",
2634     );
2635     defer store.deinit();
2636 
2637     try createItems(&store.connection);
2638     try store.connection.stage();
2639     const baseline = try store.connection.commit(&store.history);
2640 
2641     const artifact = version.ConflictArtifact.init(
2642         "items",
2643         1,
2644         "base",
2645         "ours",
2646         "theirs",
2647     );
2648     const conflicts = version.ConflictRoot.init(&.{artifact.entry()});
2649     const invalid_root = try store.connection.replaceConflictRoot(conflicts);
2650     try store.connection.stage();
2651 
2652     try std.testing.expectError(
2653         error.ConflictRootNotFound,
2654         store.connection.commit(&store.history),
2655     );
2656     try std.testing.expect(version.same(baseline, (try store.connection.checkout()).head));
2657     try std.testing.expect(version.same(baseline, (try store.history.ref("main")).?.target));
2658     try std.testing.expect(!store.history.hasDatabaseRoot(invalid_root));
2659 }
2660 
2661 test "connection commit rejects catalog identity drift" {
2662     var tmp = std.testing.tmpDir(.{});
2663     defer tmp.cleanup();
2664 
2665     var store = TestingConnection{};
2666     try store.init(std.testing.allocator, tmp.dir, "catalog-drift.db", "catalog-drift.wal", "catalog-drift.history");
2667     defer store.deinit();
2668 
2669     try createItems(&store.connection);
2670     try store.connection.stage();
2671     const baseline = try store.connection.commit(&store.history);
2672     try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'staged')");
2673     try store.connection.stage();
2674     const staged = (try store.connection.workingRoot());
2675     _ = try store.connection.catalog.createRelation(std.testing.allocator, .{
2676         .name = "untracked",
2677         .columns = &.{.{ .name = "value" }},
2678     }, .{ .durability = .buffered });
2679 
2680     try std.testing.expectError(error.InvalidHistory, store.connection.commit(&store.history));
2681     try std.testing.expect(version.same(baseline, (try store.connection.checkout()).head));
2682     try std.testing.expect(version.same(baseline, (try store.history.ref("main")).?.target));
2683     try std.testing.expect(version.same(staged, (try store.connection.workingRoot())));
2684     try std.testing.expect(!store.history.hasDatabaseRoot(staged));
2685 }
2686 
2687 test "connection fast forward missing database root is failure atomic" {
2688     var tmp = std.testing.tmpDir(.{});
2689     defer tmp.cleanup();
2690 
2691     var store = TestingConnection{};
2692     try store.init(
2693         std.testing.allocator,
2694         tmp.dir,
2695         "atomic-missing.db",
2696         "atomic-missing.wal",
2697         "atomic-missing.history",
2698     );
2699     defer store.deinit();
2700 
2701     try createItems(&store.connection);
2702     try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'before')");
2703     try store.connection.stage();
2704     const baseline_head = try store.connection.commit(&store.history);
2705     const baseline_root = (try store.connection.workingRoot());
2706 
2707     const missing_root = version.emptyHash("sql.checkout.missing.database");
2708     const parents = [_]version.Hash{baseline_head};
2709     const target = version.Commit.init(missing_root, &parents);
2710     try store.history.putCommit(target);
2711 
2712     try std.testing.expectError(
2713         error.DatabaseRootNotFound,
2714         store.connection.fastForwardBranch(std.testing.allocator, &store.history, target.hash),
2715     );
2716     try expectAtomicConnectionBaseline(&store, baseline_head, baseline_root, null);
2717 }
2718 
2719 test "connection fast forward rejects a changed working set" {
2720     var tmp = std.testing.tmpDir(.{});
2721     defer tmp.cleanup();
2722 
2723     var store = TestingConnection{};
2724     try store.init(
2725         std.testing.allocator,
2726         tmp.dir,
2727         "fast-forward-dirty.db",
2728         "fast-forward-dirty.wal",
2729         "fast-forward-dirty.history",
2730     );
2731     defer store.deinit();
2732 
2733     try createItems(&store.connection);
2734     try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'baseline')");
2735     try store.connection.stage();
2736     const baseline = try store.connection.commit(&store.history);
2737     _ = try store.connection.createBranch(&store.history, "target");
2738     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "target");
2739     try executeStatement(&store.connection, "UPDATE items SET name = 'target' WHERE rowid = 1");
2740     try store.connection.stage();
2741     const target = try store.connection.commit(&store.history);
2742     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main");
2743     try executeStatement(&store.connection, "UPDATE items SET name = 'dirty' WHERE rowid = 1");
2744     const dirty = (try store.connection.workingRoot());
2745 
2746     try std.testing.expectError(
2747         error.WorkingSetChanged,
2748         store.connection.fastForwardBranch(std.testing.allocator, &store.history, target),
2749     );
2750     try std.testing.expect(version.same(baseline, (try store.connection.checkout()).head));
2751     try std.testing.expect(version.same(baseline, (try store.history.ref("main")).?.target));
2752     try std.testing.expect(version.same(dirty, (try store.connection.workingRoot())));
2753     var result = try store.connection.execute(
2754         std.testing.allocator,
2755         "SELECT name FROM items WHERE rowid = 1",
2756         .{ .durability = .buffered },
2757     );
2758     defer result.deinit(std.testing.allocator);
2759     const view = try row.View.init(result.nextRow().?);
2760     try std.testing.expectEqualStrings("dirty", (try view.column(0)).text);
2761 }
2762 
2763 test "connection fast forward rejects an unsynced semantic clean state" {
2764     var tmp = std.testing.tmpDir(.{});
2765     defer tmp.cleanup();
2766 
2767     var store = TestingConnection{};
2768     try store.init(
2769         std.testing.allocator,
2770         tmp.dir,
2771         "fast-forward-unsynced.db",
2772         "fast-forward-unsynced.wal",
2773         "fast-forward-unsynced.history",
2774     );
2775     defer store.deinit();
2776 
2777     try createItems(&store.connection);
2778     try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'baseline')");
2779     try store.connection.stage();
2780     const baseline = try store.connection.commit(&store.history);
2781     _ = try store.connection.createBranch(&store.history, "target");
2782     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "target");
2783     try executeStatement(&store.connection, "UPDATE items SET name = 'target' WHERE rowid = 1");
2784     try store.connection.stage();
2785     const target = try store.connection.commit(&store.history);
2786     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main");
2787 
2788     try executeStatement(&store.connection, "UPDATE items SET name = 'temporary' WHERE rowid = 1");
2789     try executeStatement(&store.connection, "UPDATE items SET name = 'baseline' WHERE rowid = 1");
2790     try std.testing.expect(version.same(baseline, (try store.connection.checkout()).head));
2791     try std.testing.expect(!(try store.connection.checkout()).working.dirty());
2792     try std.testing.expect(!store.database.walSynced());
2793     try std.testing.expectError(
2794         error.WorkingSetChanged,
2795         store.connection.fastForwardBranch(std.testing.allocator, &store.history, target),
2796     );
2797     try std.testing.expect(version.same(baseline, (try store.history.ref("main")).?.target));
2798 }
2799 
2800 test "connection open repairs pending fast forward to baseline" {
2801     var tmp = std.testing.tmpDir(.{});
2802     defer tmp.cleanup();
2803 
2804     var store = TestingConnection{};
2805     try store.init(
2806         std.testing.allocator,
2807         tmp.dir,
2808         "recover-prepare.db",
2809         "recover-prepare.wal",
2810         "recover-prepare.history",
2811     );
2812     defer store.deinit();
2813 
2814     try createItems(&store.connection);
2815     try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'baseline')");
2816     try store.connection.stage();
2817     const baseline = try store.connection.commit(&store.history);
2818     _ = try store.connection.createBranch(&store.history, "target");
2819     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "target");
2820     try executeStatement(&store.connection, "UPDATE items SET name = 'target' WHERE rowid = 1");
2821     try store.connection.stage();
2822     const target = try store.connection.commit(&store.history);
2823     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main");
2824 
2825     const target_commit = try store.history.commitValue(target);
2826     var target_value = try store.history.databaseValue(
2827         std.testing.allocator,
2828         target_commit.root,
2829     );
2830     defer target_value.deinit();
2831     _ = try store.history.beginFastForward("main", baseline, target);
2832     try store.connection.materializeDatabaseValue(
2833         std.testing.allocator,
2834         &target_value,
2835         .{ .durability = .synced },
2836     );
2837     store.connection.deinit();
2838     store.connection = try Connection.open(
2839         std.testing.allocator,
2840         &store.database,
2841         &store.history,
2842         .{},
2843     );
2844 
2845     try std.testing.expect(store.history.fastForwardRecovery() == null);
2846     try std.testing.expect(version.same(baseline, (try store.history.ref("main")).?.target));
2847     try std.testing.expect(version.same(baseline, (try store.connection.checkout()).head));
2848     var result = try store.connection.execute(
2849         std.testing.allocator,
2850         "SELECT name FROM items WHERE rowid = 1",
2851         .{ .durability = .buffered },
2852     );
2853     defer result.deinit(std.testing.allocator);
2854     const view = try row.View.init(result.nextRow().?);
2855     try std.testing.expectEqualStrings("baseline", (try view.column(0)).text);
2856 }
2857 
2858 test "connection open repairs committed fast forward to target" {
2859     var tmp = std.testing.tmpDir(.{});
2860     defer tmp.cleanup();
2861 
2862     var store = TestingConnection{};
2863     try store.init(
2864         std.testing.allocator,
2865         tmp.dir,
2866         "recover-decision.db",
2867         "recover-decision.wal",
2868         "recover-decision.history",
2869     );
2870     defer store.deinit();
2871 
2872     try createItems(&store.connection);
2873     try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'baseline')");
2874     try store.connection.stage();
2875     const baseline = try store.connection.commit(&store.history);
2876     _ = try store.connection.createBranch(&store.history, "target");
2877     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "target");
2878     try executeStatement(&store.connection, "UPDATE items SET name = 'target' WHERE rowid = 1");
2879     try store.connection.stage();
2880     const target = try store.connection.commit(&store.history);
2881     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main");
2882 
2883     var update = try store.history.beginFastForward("main", baseline, target);
2884     try update.commit();
2885     store.connection.deinit();
2886     store.connection = try Connection.open(
2887         std.testing.allocator,
2888         &store.database,
2889         &store.history,
2890         .{},
2891     );
2892 
2893     try std.testing.expect(store.history.fastForwardRecovery() == null);
2894     try std.testing.expect(version.same(target, (try store.history.ref("main")).?.target));
2895     try std.testing.expect(version.same(target, (try store.connection.checkout()).head));
2896     var result = try store.connection.execute(
2897         std.testing.allocator,
2898         "SELECT name FROM items WHERE rowid = 1",
2899         .{ .durability = .buffered },
2900     );
2901     defer result.deinit(std.testing.allocator);
2902     const view = try row.View.init(result.nextRow().?);
2903     try std.testing.expectEqualStrings("target", (try view.column(0)).text);
2904 }
2905 
2906 test "connection checkout row root mismatch is failure atomic" {
2907     var tmp = std.testing.tmpDir(.{});
2908     defer tmp.cleanup();
2909 
2910     var store = TestingConnection{};
2911     try store.init(
2912         std.testing.allocator,
2913         tmp.dir,
2914         "atomic-mismatch.db",
2915         "atomic-mismatch.wal",
2916         "atomic-mismatch.history",
2917     );
2918     defer store.deinit();
2919 
2920     try createItems(&store.connection);
2921     try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'before')");
2922     try executeStatement(&store.connection, "CREATE TABLE dropme (name)");
2923     try executeStatement(&store.connection, "INSERT INTO dropme (rowid, name) VALUES (7, 'kept')");
2924     try executeStatement(&store.connection, "ANALYZE dropme");
2925     try store.connection.stage();
2926     const baseline_head = try store.connection.commit(&store.history);
2927     const baseline_root = (try store.connection.workingRoot());
2928     var baseline_dropme = try store.connection.relationView(std.testing.allocator, "dropme");
2929     const baseline_stats = baseline_dropme.root.stats.hash;
2930     baseline_dropme.deinit();
2931 
2932     try executeStatement(&store.connection, "DROP TABLE dropme");
2933     try executeStatement(&store.connection, "UPDATE items SET name = 'target' WHERE rowid = 1");
2934     var target_value = try store.connection.materializedWorkingValue(std.testing.allocator);
2935     defer target_value.deinit();
2936     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main");
2937 
2938     try executeStatement(&store.connection, "DROP TABLE dropme");
2939     try executeStatement(&store.connection, "UPDATE items SET name = 'wrong' WHERE rowid = 1");
2940     var wrong_value = try store.connection.materializedWorkingValue(std.testing.allocator);
2941     defer wrong_value.deinit();
2942     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main");
2943 
2944     const target_relation = target_value.findRelation("items") orelse
2945         return error.TestUnexpectedResult;
2946     const wrong_relation = wrong_value.findRelation("items") orelse
2947         return error.TestUnexpectedResult;
2948     try store.history.putRelationRoot(target_relation.root);
2949     try store.history.putRelationRows(target_relation.root.hash, wrong_relation.rows);
2950     try store.history.putDatabaseRoot(target_value.root);
2951     const parents = [_]version.Hash{baseline_head};
2952     const target_commit = version.Commit.init(target_value.root.hash, &parents);
2953     try store.history.putCommit(target_commit);
2954     _ = try store.history.createBranch("bad", target_commit.hash);
2955 
2956     try std.testing.expectError(
2957         error.InvalidHistory,
2958         store.connection.checkoutBranch(std.testing.allocator, &store.history, "bad"),
2959     );
2960     try expectAtomicConnectionBaseline(&store, baseline_head, baseline_root, baseline_stats);
2961     try std.testing.expect(version.same(target_commit.hash, (try store.history.ref("bad")).?.target));
2962 }
2963 
2964 test "connection fast forward allocation failures are atomic" {
2965     var tmp = std.testing.tmpDir(.{});
2966     defer tmp.cleanup();
2967 
2968     var store = TestingConnection{};
2969     try store.init(
2970         std.testing.allocator,
2971         tmp.dir,
2972         "atomic-oom.db",
2973         "atomic-oom.wal",
2974         "atomic-oom.history",
2975     );
2976     defer store.deinit();
2977 
2978     try createItems(&store.connection);
2979     try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'before')");
2980     try executeStatement(&store.connection, "CREATE TABLE dropme (name)");
2981     try executeStatement(&store.connection, "INSERT INTO dropme (rowid, name) VALUES (7, 'kept')");
2982     try executeStatement(&store.connection, "ANALYZE dropme");
2983     try store.connection.stage();
2984     const baseline_head = try store.connection.commit(&store.history);
2985     const baseline_root = (try store.connection.workingRoot());
2986     var baseline_dropme = try store.connection.relationView(std.testing.allocator, "dropme");
2987     const baseline_stats = baseline_dropme.root.stats.hash;
2988     baseline_dropme.deinit();
2989     _ = try store.connection.createBranch(&store.history, "target");
2990     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "target");
2991     try executeStatement(&store.connection, "DROP TABLE dropme");
2992     try executeStatement(&store.connection, "UPDATE items SET name = 'after' WHERE rowid = 1");
2993     try executeStatement(&store.connection, "CREATE TABLE alpha (name)");
2994     try executeStatement(&store.connection, "INSERT INTO alpha (rowid, name) VALUES (2, 'new')");
2995     try store.connection.stage();
2996     const target_head = try store.connection.commit(&store.history);
2997     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main");
2998 
2999     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
3000     const warm_start = failing.alloc_index;
3001     try store.connection.fastForwardBranch(failing.allocator(), &store.history, target_head);
3002     const operation_allocations = failing.alloc_index - warm_start;
3003     try std.testing.expect(operation_allocations > 0);
3004     try store.history.putRef(.{ .name = "main", .target = baseline_head });
3005     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main");
3006 
3007     const offsets = [_]usize{ 0, operation_allocations / 2, operation_allocations - 1 };
3008     for (offsets) |offset| {
3009         failing.fail_index = failing.alloc_index + offset;
3010         failing.resize_fail_index = std.math.maxInt(usize);
3011         try std.testing.expectError(
3012             error.OutOfMemory,
3013             store.connection.fastForwardBranch(failing.allocator(), &store.history, target_head),
3014         );
3015         try std.testing.expect(failing.has_induced_failure);
3016         failing.fail_index = std.math.maxInt(usize);
3017         failing.resize_fail_index = std.math.maxInt(usize);
3018         failing.has_induced_failure = false;
3019         try expectAtomicConnectionBaseline(&store, baseline_head, baseline_root, baseline_stats);
3020     }
3021 }
3022 
3023 const TestingConnection = struct {
3024     database: file.Database = undefined,
3025     history: history_mod.History = undefined,
3026     connection: Connection = undefined,
3027 
3028     fn init(self: *TestingConnection, allocator: Allocator, dir: std.Io.Dir, database_path: []const u8, wal_path: []const u8, history_path: []const u8) !void {
3029         self.database = try file.Database.openForTesting(allocator, dir, .{
3030             .paths = .{ .database = database_path, .wal = wal_path },
3031             .header = testingHeader(),
3032         });
3033         errdefer self.database.deinit();
3034         try self.database.reserve(.{ .wal_frames = 960 });
3035 
3036         self.history = try history_mod.History.open(allocator, dir, .{ .path = history_path, .recovery = .reject });
3037         errdefer self.history.deinit();
3038         self.connection = try Connection.create(allocator, &self.database, &self.history, .{});
3039     }
3040 
3041     fn deinit(self: *TestingConnection) void {
3042         self.connection.deinit();
3043         self.history.deinit();
3044         self.database.deinit();
3045         self.* = undefined;
3046     }
3047 };
3048 
3049 const TestingConflict = struct {
3050     commit: version.Hash,
3051     root: version.Hash,
3052     artifact: version.Hash,
3053     database: version.Hash,
3054 };
3055 
3056 fn commitTestingConflict(store: *TestingConnection) !TestingConflict {
3057     try createItems(&store.connection);
3058     try executeStatement(&store.connection, "INSERT INTO items (rowid, name) VALUES (1, 'base')");
3059     try store.connection.stage();
3060     _ = try store.connection.commit(&store.history);
3061     _ = try store.connection.createBranch(&store.history, "side");
3062     _ = try store.connection.createBranch(&store.history, "behind");
3063     try executeStatement(&store.connection, "UPDATE items SET name = 'ours' WHERE rowid = 1");
3064     try store.connection.stage();
3065     _ = try store.connection.commit(&store.history);
3066     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "side");
3067     try executeStatement(&store.connection, "UPDATE items SET name = 'theirs' WHERE rowid = 1");
3068     try store.connection.stage();
3069     const side_commit = try store.connection.commit(&store.history);
3070     try store.connection.checkoutBranch(std.testing.allocator, &store.history, "main");
3071     var merged = try store.connection.mergeBranch(std.testing.allocator, &store.history, "side", .{});
3072     defer merged.deinit();
3073     try std.testing.expect(merged.hasConflicts());
3074     try std.testing.expectEqual(@as(usize, 1), merged.conflict_root.count);
3075     const root = merged.conflict_root.hash;
3076     const artifact = merged.discovered[0].artifact.hash;
3077     try store.connection.stage();
3078     const commit = try store.connection.mergeCommit(&store.history, side_commit);
3079     return .{ .commit = commit, .root = root, .artifact = artifact, .database = (try store.connection.workingRoot()) };
3080 }
3081 
3082 fn expectTestingConflict(store: *TestingConnection, conflict: TestingConflict) !void {
3083     try std.testing.expect(version.same(conflict.root, store.connection.session.workingRoot().conflicts));
3084     var artifacts = try store.connection.conflictArtifacts(std.testing.allocator, &store.history);
3085     defer artifacts.deinit();
3086     try std.testing.expectEqual(@as(usize, 1), artifacts.artifacts.len);
3087     try std.testing.expect(version.same(conflict.artifact, artifacts.artifacts[0].hash));
3088 }
3089 
3090 fn createItems(connection: *Connection) !void {
3091     try executeStatement(connection, "CREATE TABLE items (name)");
3092 }
3093 
3094 fn executeStatement(connection: *Connection, source: []const u8) !void {
3095     var result = try connection.execute(std.testing.allocator, source, .{ .durability = .buffered });
3096     defer result.deinit(std.testing.allocator);
3097 }
3098 
3099 fn expectLiveDatabaseRoot(connection: *Connection, target: *const version.DatabaseRoot) !void {
3100     var live = try version.databaseRoot(
3101         std.testing.allocator,
3102         &connection.catalog,
3103         target.conflicts,
3104     );
3105     defer live.deinit();
3106     try std.testing.expect(version.same(live.hash, target.hash));
3107 }
3108 
3109 fn expectAtomicConnectionBaseline(
3110     store: *TestingConnection,
3111     head: version.Hash,
3112     root: version.Hash,
3113     dropme_stats: ?version.Hash,
3114 ) !void {
3115     const checkout_value = (try store.connection.checkout());
3116     try std.testing.expectEqualStrings("main", checkout_value.name);
3117     try std.testing.expect(version.same(head, checkout_value.head));
3118     try std.testing.expect(version.same(head, (try store.history.ref("main")).?.target));
3119     try std.testing.expect(version.same(root, checkout_value.working.base));
3120     try std.testing.expect(version.same(root, checkout_value.working.working));
3121     try std.testing.expect(version.same(root, checkout_value.working.staged));
3122     try std.testing.expect(version.same(root, (try store.connection.workingRoot())));
3123 
3124     var item = try store.connection.execute(
3125         std.testing.allocator,
3126         "SELECT name FROM items WHERE rowid = 1",
3127         .{ .durability = .buffered },
3128     );
3129     defer item.deinit(std.testing.allocator);
3130     try std.testing.expectEqual(@as(usize, 1), item.rowCount());
3131     const item_view = try row.View.init(item.nextRow().?);
3132     try std.testing.expectEqualStrings("before", (try item_view.column(0)).text);
3133 
3134     if (dropme_stats) |stats_hash| {
3135         var kept = try store.connection.execute(
3136             std.testing.allocator,
3137             "SELECT name FROM dropme WHERE rowid = 7",
3138             .{ .durability = .buffered },
3139         );
3140         defer kept.deinit(std.testing.allocator);
3141         try std.testing.expectEqual(@as(usize, 1), kept.rowCount());
3142         const kept_view = try row.View.init(kept.nextRow().?);
3143         try std.testing.expectEqualStrings("kept", (try kept_view.column(0)).text);
3144         var stats = (try store.connection.catalog.relationStats(std.testing.allocator, "dropme")).?;
3145         defer stats.deinit();
3146         try std.testing.expectEqual(@as(usize, 1), stats.table.entries);
3147         var relation_view = try store.connection.relationView(std.testing.allocator, "dropme");
3148         defer relation_view.deinit();
3149         try std.testing.expect(version.same(stats_hash, relation_view.root.stats.hash));
3150     }
3151 
3152     var live_root = try version.databaseRootMaintained(
3153         std.testing.allocator,
3154         &store.connection.catalog,
3155         store.connection.session.workingRoot().conflicts,
3156     );
3157     defer live_root.deinit();
3158     try std.testing.expect(version.same(root, live_root.hash));
3159 }
3160 
3161 fn testEntryHash(root: *const version.DatabaseRoot, name: []const u8) version.Hash {
3162     return testFindEntry(root, name) orelse unreachable;
3163 }
3164 
3165 fn testFindEntry(root: *const version.DatabaseRoot, name: []const u8) ?version.Hash {
3166     for (root.entries) |entry| {
3167         if (std.mem.eql(u8, entry.name, name)) return entry.hash;
3168     }
3169     return null;
3170 }
3171 
3172 fn hasConflictHash(entries: []const version.ConflictEntry, hash: version.Hash) bool {
3173     for (entries) |entry| {
3174         if (version.same(entry.hash, hash)) return true;
3175     }
3176     return false;
3177 }
3178 
3179 fn hasConflictHashValue(hashes: []const version.Hash, hash: version.Hash) bool {
3180     for (hashes) |candidate| {
3181         if (version.same(candidate, hash)) return true;
3182     }
3183     return false;
3184 }
3185 
3186 fn testingHeader() wal.Header {
3187     return .{
3188         .sequence = 3901,
3189         .salt = .{ .first = 0x1357_3901, .second = 0x2468_3901 },
3190     };
3191 }