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 }