lib/sql/src/session/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const sql = @import("../root.zig");
4 const session = @import("root.zig");
5
6 const branch = sql.branch;
7 const catalog_mod = sql.catalog;
8 const file = sql.file;
9 const history_mod = sql.history;
10 const relation_mod = sql.relation;
11 const row = sql.row;
12 const version = sql.version;
13 const wal = sql.wal;
14
15 const Allocator = std.mem.Allocator;
16 const Error = session.Error;
17 const DatabaseSession = session.DatabaseSession;
18 const DatabaseWrite = session.DatabaseWrite;
19 const RelationSession = session.RelationSession;
20
21 const testing_write_limits = DatabaseWrite.Limits{
22 .relations = 4,
23 .edits = 8,
24 .payload_bytes = 512,
25 .assignments = 8,
26 };
27
28 fn testingDatabaseRoot(
29 allocator: Allocator,
30 catalog: *const catalog_mod.Catalog,
31 ) Error!version.DatabaseRoot {
32 return try version.databaseRootMaintained(
33 allocator,
34 catalog,
35 version.ConflictRoot.empty().hash,
36 );
37 }
38
39 test "relation sessions stage edits through database flush" {
40 var tmp = std.testing.tmpDir(.{});
41 defer tmp.cleanup();
42
43 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
44 .paths = .{ .database = "session.db", .wal = "session.wal" },
45 .header = testingHeader(),
46 });
47 defer database.deinit();
48 try database.reserve(.{ .wal_frames = 512 });
49
50 var catalog = try catalog_mod.Catalog.open(&database, .{});
51 _ = try catalog.createRelation(std.testing.allocator, .{
52 .name = "items",
53 .columns = &.{.{ .name = "value" }},
54 }, .{ .durability = .buffered });
55 _ = try catalog.createRelation(std.testing.allocator, .{
56 .name = "users",
57 .columns = &.{.{ .name = "name" }},
58 }, .{ .durability = .buffered });
59
60 var relation_session = try RelationSession.open(std.testing.allocator, &catalog, "items");
61 const before_relation = relation_session.root.hash;
62 var relation_session_live = true;
63 errdefer if (relation_session_live) relation_session.deinit();
64
65 var root = try testingDatabaseRoot(std.testing.allocator, &catalog);
66 const commit = version.Commit.init(root.hash, &.{});
67 var database_session = DatabaseSession.initWithRoot(std.testing.allocator, .{
68 .name = "main",
69 .head = commit.hash,
70 .working = version.WorkingSet.init(root.hash),
71 }, &root);
72 defer database_session.deinit();
73
74 try relation_session.put(1, &.{.{ .text = "one" }});
75 try std.testing.expectEqual(@as(usize, 1), relation_session.pendingEdits());
76 try std.testing.expect(version.same(before_relation, relation_session.root.hash));
77
78 var workspace = try DatabaseWrite.Workspace.allocate(
79 std.testing.allocator,
80 testing_write_limits,
81 );
82 defer workspace.deallocate(std.testing.allocator);
83 var write = try database_session.beginWrite(
84 &workspace,
85 std.testing.allocator,
86 testing_write_limits,
87 .{ .durability = .buffered },
88 );
89 defer write.deinit();
90 try write.stageRelation(&relation_session);
91 relation_session_live = false;
92 var database_flush = try write.flush();
93 defer database_flush.deinit();
94 const first_flush = database_flush.onlyRelation();
95
96 var relation_after = try RelationSession.open(std.testing.allocator, &catalog, "items");
97 defer relation_after.deinit();
98 try std.testing.expect(!version.same(before_relation, relation_after.root.hash));
99 try std.testing.expect(version.same(relation_after.root.hash, first_flush.relation));
100 try std.testing.expect(version.same(database_session.checkout.working.working, first_flush.database));
101 try std.testing.expectEqual(@as(usize, 1), relation_after.root.table.summary.entries);
102 try std.testing.expectEqual(relation_after.schema, try catalog.schemaState(std.testing.allocator));
103
104 const found = (try relation_after.handle.relation.get(std.testing.allocator, 1)).?;
105 defer std.testing.allocator.free(found);
106 const view = try row.View.init(found);
107 try std.testing.expectEqualStrings("one", (try view.column(0)).text);
108 }
109
110 test "database write appends staged edits to staged relations" {
111 var tmp = std.testing.tmpDir(.{});
112 defer tmp.cleanup();
113
114 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
115 .paths = .{ .database = "database-write-staged-edits.db", .wal = "database-write-staged-edits.wal" },
116 .header = testingHeader(),
117 });
118 defer database.deinit();
119 try database.reserve(.{ .wal_frames = 512 });
120
121 var catalog = try catalog_mod.Catalog.open(&database, .{});
122 _ = try catalog.createRelation(std.testing.allocator, .{
123 .name = "items",
124 .columns = &.{ .{ .name = "name" }, .{ .name = "score" } },
125 }, .{ .durability = .buffered });
126
127 var root = try testingDatabaseRoot(std.testing.allocator, &catalog);
128 const commit = version.Commit.init(root.hash, &.{});
129 const root_hash = root.hash;
130 var database_session = DatabaseSession.initWithRoot(std.testing.allocator, branch.checkout(.{
131 .name = "main",
132 .target = commit.hash,
133 }, root_hash), &root);
134 defer database_session.deinit();
135
136 var workspace = try DatabaseWrite.Workspace.allocate(
137 std.testing.allocator,
138 testing_write_limits,
139 );
140 defer workspace.deallocate(std.testing.allocator);
141 var write = try database_session.beginWrite(
142 &workspace,
143 std.testing.allocator,
144 testing_write_limits,
145 .{ .durability = .buffered },
146 );
147 defer write.deinit();
148 try std.testing.expect(write.stagedRelationRoot("items") == null);
149 try std.testing.expectError(error.NoStagedRelation, write.stagePut("items", 1, &.{ .{ .text = "ada" }, .{ .integer = 1 } }));
150
151 var relation_session = try RelationSession.open(std.testing.allocator, &catalog, "items");
152 const base_root = relation_session.root.hash;
153 var relation_session_live = true;
154 errdefer if (relation_session_live) relation_session.deinit();
155 try relation_session.put(1, &.{ .{ .text = "ada" }, .{ .integer = 1 } });
156 try write.stageRelation(&relation_session);
157 relation_session_live = false;
158
159 try std.testing.expect(version.same(base_root, write.stagedRelationRoot("items").?.hash));
160 try write.stagePut("items", 2, &.{ .{ .text = "grace" }, .{ .integer = 2 } });
161 try write.stagePut("items", 3, &.{ .{ .text = "ida" }, .{ .integer = 3 } });
162 try write.stageUpdate("items", 2, &.{.{ .column = 1, .value = .{ .integer = 20 } }});
163 try write.stageDelete("items", 3);
164 try std.testing.expectEqual(@as(usize, 1), write.pendingRelations());
165
166 var flush = try write.flush();
167 defer flush.deinit();
168
169 var handle = try catalog.openRelation(std.testing.allocator, "items");
170 defer handle.deinit();
171 const first = (try handle.relation.get(std.testing.allocator, 1)).?;
172 defer std.testing.allocator.free(first);
173 const first_view = try row.View.init(first);
174 try std.testing.expectEqualStrings("ada", (try first_view.column(0)).text);
175 const second = (try handle.relation.get(std.testing.allocator, 2)).?;
176 defer std.testing.allocator.free(second);
177 const second_view = try row.View.init(second);
178 try std.testing.expectEqualStrings("grace", (try second_view.column(0)).text);
179 try std.testing.expectEqual(@as(i64, 20), (try second_view.column(1)).integer);
180 const third = try handle.relation.get(std.testing.allocator, 3);
181 if (third) |bytes| std.testing.allocator.free(bytes);
182 try std.testing.expect(third == null);
183 }
184
185 test "database write limits bound staged edit storage" {
186 comptime {
187 @stardustClaim(
188 @import("alloc_phase").capacity.witness(@import("./staging/root.zig").Storage, "sql_database_write_staging_semantics_overload"),
189 null,
190 null,
191 null,
192 null,
193 null,
194 null,
195 );
196 }
197 comptime {
198 @stardustClaim(
199 @import("alloc_phase").capacity.witness(@import("./staging/root.zig").Storage, "sql_database_write_staging_semantics_transitive_risk"),
200 null,
201 null,
202 null,
203 null,
204 null,
205 null,
206 );
207 }
208 comptime {
209 @stardustClaim(
210 @import("alloc_phase").capacity.witness(@import("./staging/root.zig").Storage, "sql_database_write_staging_semantics_foreign_risk"),
211 null,
212 null,
213 null,
214 null,
215 null,
216 null,
217 );
218 }
219
220 var tmp = std.testing.tmpDir(.{});
221 defer tmp.cleanup();
222
223 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
224 .paths = .{ .database = "database-write-bounds.db", .wal = "database-write-bounds.wal" },
225 .header = testingHeader(),
226 });
227 defer database.deinit();
228 try database.reserve(.{ .wal_frames = 512 });
229
230 var catalog = try catalog_mod.Catalog.open(&database, .{});
231 _ = try catalog.createRelation(std.testing.allocator, .{
232 .name = "items",
233 .columns = &.{ .{ .name = "name" }, .{ .name = "score" } },
234 }, .{ .durability = .buffered });
235 _ = try catalog.createRelation(std.testing.allocator, .{
236 .name = "other",
237 .columns = &.{.{ .name = "name" }},
238 }, .{ .durability = .buffered });
239
240 var root = try testingDatabaseRoot(std.testing.allocator, &catalog);
241 const commit = version.Commit.init(root.hash, &.{});
242 var database_session = DatabaseSession.initWithRoot(std.testing.allocator, branch.checkout(.{
243 .name = "main",
244 .target = commit.hash,
245 }, root.hash), &root);
246 defer database_session.deinit();
247
248 var items = try RelationSession.open(std.testing.allocator, &catalog, "items");
249 var items_live = true;
250 errdefer if (items_live) items.deinit();
251 var other = try RelationSession.open(std.testing.allocator, &catalog, "other");
252 defer other.deinit();
253
254 const put_values = [_]row.Value{ .{ .text = "bounded" }, .{ .integer = 7 } };
255 const update_text = "updated";
256 const put_bytes = try row.encodedSize(&put_values);
257 const limits = DatabaseWrite.Limits{
258 .relations = 1,
259 .edits = 4,
260 .payload_bytes = put_bytes + update_text.len,
261 .assignments = 1,
262 };
263 var phase_allocator = try alloc_phase.SealedPhaseAllocator.init(std.testing.allocator);
264 var maybe_write: ?DatabaseWrite = null;
265 const phase_memory = phase_allocator.initializationAllocator();
266 var workspace = try DatabaseWrite.Workspace.allocate(phase_memory, limits);
267 var workspace_live = true;
268 errdefer {
269 if (phase_allocator.phase() == .initialization) phase_allocator.abortInitialization();
270 if (phase_allocator.phase() == .steady) phase_allocator.beginTeardown();
271 if (maybe_write) |*write| write.deinit();
272 if (workspace_live) {
273 workspace.deallocate(phase_allocator.teardownAllocator());
274 }
275 phase_allocator.deinit();
276 }
277 phase_allocator.seal();
278 maybe_write = try database_session.beginWrite(
279 &workspace,
280 phase_memory,
281 limits,
282 .{ .durability = .buffered },
283 );
284 const write = &maybe_write.?;
285
286 try write.stageRelation(&items);
287 items_live = false;
288 try std.testing.expectError(error.TooManyStagedRelations, write.stageRelation(&other));
289 try write.stagePut("items", 1, &put_values);
290 try write.stageUpdate("items", 1, &.{.{ .column = 0, .value = .{ .text = update_text } }});
291 try std.testing.expectError(
292 error.TooManyStagedAssignments,
293 write.stageUpdate("items", 1, &.{.{ .column = 1, .value = .nil }}),
294 );
295 try std.testing.expectError(
296 error.StagedPayloadTooLarge,
297 write.stagePut("items", 2, &.{ .{ .text = "overflow" }, .{ .integer = 8 } }),
298 );
299 try write.stageDelete("items", 2);
300 try write.stageDelete("items", 3);
301 try std.testing.expectError(error.TooManyStagedEdits, write.stageDelete("items", 4));
302
303 try std.testing.expectEqual(@as(usize, 1), write.pendingRelations());
304 var edits = write.stagedRelationEdits("items").?;
305 const expected_tags = [_]std.meta.Tag(relation_mod.Edit){ .put, .update, .delete, .delete };
306 for (expected_tags) |expected| {
307 try std.testing.expectEqual(expected, std.meta.activeTag(edits.next().?));
308 }
309 try std.testing.expect(edits.next() == null);
310 try std.testing.expectEqual(alloc_phase.PhaseViolations{}, phase_allocator.violations());
311
312 write.deinit();
313 maybe_write = null;
314 maybe_write = try database_session.beginWrite(
315 &workspace,
316 phase_memory,
317 limits,
318 .{ .durability = .buffered },
319 );
320 maybe_write.?.deinit();
321 maybe_write = null;
322 try std.testing.expectEqual(alloc_phase.PhaseViolations{}, phase_allocator.violations());
323
324 phase_allocator.beginTeardown();
325 workspace.deallocate(phase_allocator.teardownAllocator());
326 workspace_live = false;
327 try std.testing.expectEqual(alloc_phase.PhaseViolations{}, phase_allocator.violations());
328 phase_allocator.deinit();
329 }
330
331 test "database write can trust staged relation indexes" {
332 var tmp = std.testing.tmpDir(.{});
333 defer tmp.cleanup();
334
335 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
336 .paths = .{ .database = "database-write-trusted-indexes.db", .wal = "database-write-trusted-indexes.wal" },
337 .header = testingHeader(),
338 });
339 defer database.deinit();
340 try database.reserve(.{ .wal_frames = 512 });
341
342 var catalog = try catalog_mod.Catalog.open(&database, .{});
343 const columns = [_]catalog_mod.ColumnDefinition{
344 .{ .name = "name" },
345 .{ .name = "score" },
346 };
347 const indexes = [_]catalog_mod.IndexDefinition{.{
348 .name = "items_score",
349 .fields = &.{1},
350 }};
351 _ = try catalog.createRelation(std.testing.allocator, .{
352 .name = "items",
353 .columns = &columns,
354 .indexes = &indexes,
355 }, .{ .durability = .buffered });
356
357 {
358 var handle = try catalog.openRelation(std.testing.allocator, "items");
359 defer handle.deinit();
360 _ = try handle.relation.put(std.testing.allocator, 1, &.{ .{ .text = "ada" }, .{ .integer = 7 } }, .{ .durability = .buffered });
361 _ = try handle.relation.indexes[0].index.delete(1, &.{.{ .integer = 7 }}, .{ .durability = .buffered });
362 try std.testing.expectError(error.SecondaryIndexCorrupt, handle.relation.validateIndexes(std.testing.allocator));
363 }
364
365 var root = try testingDatabaseRoot(std.testing.allocator, &catalog);
366 const commit = version.Commit.init(root.hash, &.{});
367 const root_hash = root.hash;
368 var database_session = DatabaseSession.initWithRoot(std.testing.allocator, branch.checkout(.{
369 .name = "main",
370 .target = commit.hash,
371 }, root_hash), &root);
372 defer database_session.deinit();
373
374 var workspace = try DatabaseWrite.Workspace.allocate(
375 std.testing.allocator,
376 testing_write_limits,
377 );
378 defer workspace.deallocate(std.testing.allocator);
379 {
380 var relation_session = try RelationSession.open(std.testing.allocator, &catalog, "items");
381 var relation_session_live = true;
382 errdefer if (relation_session_live) relation_session.deinit();
383 try relation_session.put(2, &.{ .{ .text = "grace" }, .{ .integer = 9 } });
384 var write = try database_session.beginWrite(&workspace, std.testing.allocator, testing_write_limits, .{
385 .durability = .buffered,
386 .validate_indexes = true,
387 });
388 defer write.deinit();
389 try write.stageRelation(&relation_session);
390 relation_session_live = false;
391 try std.testing.expectError(error.SecondaryIndexCorrupt, write.flush());
392 }
393
394 {
395 var relation_session = try RelationSession.open(std.testing.allocator, &catalog, "items");
396 var relation_session_live = true;
397 errdefer if (relation_session_live) relation_session.deinit();
398 try relation_session.put(2, &.{ .{ .text = "grace" }, .{ .integer = 9 } });
399 var write = try database_session.beginWrite(&workspace, std.testing.allocator, testing_write_limits, .{
400 .durability = .buffered,
401 .validate_indexes = false,
402 });
403 defer write.deinit();
404 try write.stageRelation(&relation_session);
405 relation_session_live = false;
406 var flush = try write.flush();
407 defer flush.deinit();
408 }
409
410 var handle = try catalog.openRelation(std.testing.allocator, "items");
411 defer handle.deinit();
412 const found = (try handle.relation.get(std.testing.allocator, 2)).?;
413 defer std.testing.allocator.free(found);
414 const view = try row.View.init(found);
415 try std.testing.expectEqualStrings("grace", (try view.column(0)).text);
416 }
417
418 test "database session rejects staged relations that drift from working values" {
419 var tmp = std.testing.tmpDir(.{});
420 defer tmp.cleanup();
421
422 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
423 .paths = .{ .database = "database-session-stale-relation.db", .wal = "database-session-stale-relation.wal" },
424 .header = testingHeader(),
425 });
426 defer database.deinit();
427 try database.reserve(.{ .wal_frames = 512 });
428
429 var catalog = try catalog_mod.Catalog.open(&database, .{});
430 _ = try catalog.createRelation(std.testing.allocator, .{
431 .name = "items",
432 .columns = &.{.{ .name = "value" }},
433 }, .{ .durability = .buffered });
434
435 var root = try testingDatabaseRoot(std.testing.allocator, &catalog);
436 const commit = version.Commit.init(root.hash, &.{});
437 const root_hash = root.hash;
438 var database_session = DatabaseSession.initWithRoot(std.testing.allocator, branch.checkout(.{
439 .name = "main",
440 .target = commit.hash,
441 }, root_hash), &root);
442 defer database_session.deinit();
443
444 var relation_session = try RelationSession.open(std.testing.allocator, &catalog, "items");
445 var relation_session_live = true;
446 errdefer if (relation_session_live) relation_session.deinit();
447 try relation_session.put(2, &.{.{ .text = "session" }});
448
449 var live = try catalog.openRelation(std.testing.allocator, "items");
450 defer live.deinit();
451 _ = try live.relation.put(std.testing.allocator, 99, &.{.{ .text = "external" }}, .{ .durability = .buffered });
452
453 var workspace = try DatabaseWrite.Workspace.allocate(
454 std.testing.allocator,
455 testing_write_limits,
456 );
457 defer workspace.deallocate(std.testing.allocator);
458 var write = try database_session.beginWrite(
459 &workspace,
460 std.testing.allocator,
461 testing_write_limits,
462 .{ .durability = .buffered },
463 );
464 defer write.deinit();
465 try write.stageRelation(&relation_session);
466 relation_session_live = false;
467 try std.testing.expectError(error.StagedRelationRootMismatch, write.flush());
468
469 const missing = try live.relation.get(std.testing.allocator, 2);
470 if (missing) |bytes| std.testing.allocator.free(bytes);
471 try std.testing.expect(missing == null);
472 const external = (try live.relation.get(std.testing.allocator, 99)).?;
473 defer std.testing.allocator.free(external);
474 const view = try row.View.init(external);
475 try std.testing.expectEqualStrings("external", (try view.column(0)).text);
476 }
477
478 test "database session stages flushed database roots before history commits" {
479 var tmp = std.testing.tmpDir(.{});
480 defer tmp.cleanup();
481
482 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
483 .paths = .{ .database = "database-session.db", .wal = "database-session.wal" },
484 .header = testingHeader(),
485 });
486 defer database.deinit();
487 try database.reserve(.{ .wal_frames = 512 });
488
489 var catalog = try catalog_mod.Catalog.open(&database, .{});
490 _ = try catalog.createRelation(std.testing.allocator, .{
491 .name = "items",
492 .columns = &.{.{ .name = "value" }},
493 }, .{ .durability = .buffered });
494
495 var database_root_value = try testingDatabaseRoot(std.testing.allocator, &catalog);
496 const database_root = database_root_value.hash;
497
498 var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "database-session.history", .recovery = .reject });
499 defer history.deinit();
500 const root_commit = version.Commit.init(database_root, &.{});
501 try history.putCommit(root_commit);
502 _ = try history.createBranch("main", root_commit.hash);
503
504 var database_session = DatabaseSession.initWithRoot(std.testing.allocator, try history.checkoutBranch("main"), &database_root_value);
505 defer database_session.deinit();
506 try std.testing.expect(version.same(root_commit.hash, database_session.checkout.head));
507 try std.testing.expect(version.same(database_root, database_session.checkout.working.base));
508
509 var relation_session = try RelationSession.open(std.testing.allocator, &catalog, "items");
510 var relation_session_live = true;
511 errdefer if (relation_session_live) relation_session.deinit();
512
513 try relation_session.put(1, &.{.{ .text = "one" }});
514 var workspace = try DatabaseWrite.Workspace.allocate(
515 std.testing.allocator,
516 testing_write_limits,
517 );
518 defer workspace.deallocate(std.testing.allocator);
519 var write = try database_session.beginWrite(
520 &workspace,
521 std.testing.allocator,
522 testing_write_limits,
523 .{ .durability = .buffered },
524 );
525 defer write.deinit();
526 try write.stageRelation(&relation_session);
527 relation_session_live = false;
528 try std.testing.expectEqual(@as(usize, 1), write.pendingRelations());
529 var database_flush = try write.flush();
530 defer database_flush.deinit();
531 const flush = database_flush.onlyRelation();
532 try std.testing.expectEqual(@as(usize, 0), database_session.pendingRelations());
533 const working_root = database_session.workingRoot();
534 try std.testing.expect(version.same(flush.database, working_root.hash));
535 try std.testing.expectEqual(@as(usize, 1), working_root.entries.len);
536 try std.testing.expect(version.same(flush.relation, working_root.entries[0].hash));
537 var flushed_items = try RelationSession.open(std.testing.allocator, &catalog, "items");
538 defer flushed_items.deinit();
539 try std.testing.expectEqual(@as(usize, 1), flushed_items.root.table.summary.entries);
540
541 try std.testing.expect(version.same(root_commit.hash, database_session.checkout.head));
542 try std.testing.expect(version.same(root_commit.hash, (try history.ref("main")).?.target));
543 try std.testing.expect(version.same(root_commit.root, database_session.checkout.working.base));
544 try std.testing.expect(version.same(flush.database, database_session.checkout.working.working));
545 try std.testing.expect(database_session.checkout.working.dirty());
546 try std.testing.expect(!database_session.checkout.working.hasStaged());
547 try std.testing.expectError(error.NoStagedRoot, database_session.commit(&history));
548
549 database_session.stage();
550 try std.testing.expect(database_session.checkout.working.hasStaged());
551 const commit_hash = try database_session.commit(&history);
552 try std.testing.expect(version.same(commit_hash, (try history.ref("main")).?.target));
553 try std.testing.expect(version.same(commit_hash, database_session.checkout.head));
554 try std.testing.expect(version.same(flush.database, database_session.checkout.working.base));
555 try std.testing.expect(version.same(flush.database, database_session.workingRoot().hash));
556 try std.testing.expect(!database_session.checkout.working.dirty());
557 try std.testing.expect(!database_session.checkout.working.hasStaged());
558
559 const checkout = try history.checkoutBranch("main");
560 try std.testing.expect(version.same(commit_hash, checkout.head));
561 try std.testing.expect(version.same(flush.database, checkout.working.base));
562 }
563
564 test "database session flushes queued relation edits into one working root" {
565 var tmp = std.testing.tmpDir(.{});
566 defer tmp.cleanup();
567
568 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
569 .paths = .{ .database = "database-session-queue.db", .wal = "database-session-queue.wal" },
570 .header = testingHeader(),
571 });
572 defer database.deinit();
573 try database.reserve(.{ .wal_frames = 768 });
574
575 var catalog = try catalog_mod.Catalog.open(&database, .{});
576 _ = try catalog.createRelation(std.testing.allocator, .{
577 .name = "items",
578 .columns = &.{.{ .name = "value" }},
579 }, .{ .durability = .buffered });
580 _ = try catalog.createRelation(std.testing.allocator, .{
581 .name = "users",
582 .columns = &.{.{ .name = "name" }},
583 }, .{ .durability = .buffered });
584
585 var database_root_value = try testingDatabaseRoot(std.testing.allocator, &catalog);
586 const database_root = database_root_value.hash;
587
588 var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "database-session-queue.history", .recovery = .reject });
589 defer history.deinit();
590 const root_commit = version.Commit.init(database_root, &.{});
591 try history.putCommit(root_commit);
592 _ = try history.createBranch("main", root_commit.hash);
593
594 var database_session = DatabaseSession.initWithRoot(std.testing.allocator, try history.checkoutBranch("main"), &database_root_value);
595 defer database_session.deinit();
596 var items_session = try RelationSession.open(std.testing.allocator, &catalog, "items");
597 var items_session_live = true;
598 errdefer if (items_session_live) items_session.deinit();
599 var users_session = try RelationSession.open(std.testing.allocator, &catalog, "users");
600 var users_session_live = true;
601 errdefer if (users_session_live) users_session.deinit();
602
603 const before_items = items_session.root.hash;
604 const before_users = users_session.root.hash;
605 const before_database = database_session.checkout.working.working;
606
607 try items_session.put(1, &.{.{ .text = "bolt" }});
608 try users_session.put(7, &.{.{ .text = "ada" }});
609 var workspace = try DatabaseWrite.Workspace.allocate(
610 std.testing.allocator,
611 testing_write_limits,
612 );
613 defer workspace.deallocate(std.testing.allocator);
614 var write = try database_session.beginWrite(
615 &workspace,
616 std.testing.allocator,
617 testing_write_limits,
618 .{ .durability = .buffered },
619 );
620 errdefer write.deinit();
621 try write.stageRelation(&items_session);
622 items_session_live = false;
623 try write.stageRelation(&users_session);
624 users_session_live = false;
625 try std.testing.expectEqual(@as(usize, 2), write.pendingRelations());
626 try std.testing.expect(version.same(before_database, database_session.checkout.working.working));
627
628 var flush = try write.flush();
629 defer flush.deinit();
630 var items_after = try RelationSession.open(std.testing.allocator, &catalog, "items");
631 defer items_after.deinit();
632 var users_after = try RelationSession.open(std.testing.allocator, &catalog, "users");
633 defer users_after.deinit();
634
635 try std.testing.expectEqual(@as(usize, 2), flush.relations.len);
636 try std.testing.expectEqual(@as(usize, 0), database_session.pendingRelations());
637 try std.testing.expect(!version.same(before_items, items_after.root.hash));
638 try std.testing.expect(!version.same(before_users, users_after.root.hash));
639 try std.testing.expect(!version.same(before_database, flush.database));
640 try std.testing.expect(version.same(flush.database, database_session.checkout.working.working));
641 try std.testing.expect(version.same(flush.database, database_session.workingRoot().hash));
642 try std.testing.expect(version.same(flush.database, flush.relations[0].database));
643 try std.testing.expect(version.same(flush.database, flush.relations[1].database));
644 try std.testing.expect(version.same(items_after.root.hash, flush.relations[0].relation));
645 try std.testing.expect(version.same(users_after.root.hash, flush.relations[1].relation));
646
647 const item = (try items_after.handle.relation.get(std.testing.allocator, 1)).?;
648 defer std.testing.allocator.free(item);
649 const item_view = try row.View.init(item);
650 try std.testing.expectEqualStrings("bolt", (try item_view.column(0)).text);
651 const user = (try users_after.handle.relation.get(std.testing.allocator, 7)).?;
652 defer std.testing.allocator.free(user);
653 const user_view = try row.View.init(user);
654 try std.testing.expectEqualStrings("ada", (try user_view.column(0)).text);
655 }
656
657 test "database session assembles staged flushes from working database value" {
658 var tmp = std.testing.tmpDir(.{});
659 defer tmp.cleanup();
660
661 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
662 .paths = .{ .database = "database-session-value-base.db", .wal = "database-session-value-base.wal" },
663 .header = testingHeader(),
664 });
665 defer database.deinit();
666 try database.reserve(.{ .wal_frames = 768 });
667
668 var catalog = try catalog_mod.Catalog.open(&database, .{});
669 _ = try catalog.createRelation(std.testing.allocator, .{
670 .name = "items",
671 .columns = &.{.{ .name = "value" }},
672 }, .{ .durability = .buffered });
673 _ = try catalog.createRelation(std.testing.allocator, .{
674 .name = "users",
675 .columns = &.{.{ .name = "name" }},
676 }, .{ .durability = .buffered });
677
678 var database_root_value = try testingDatabaseRoot(std.testing.allocator, &catalog);
679
680 var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "database-session-value-base.history", .recovery = .reject });
681 defer history.deinit();
682 const root_commit = version.Commit.init(database_root_value.hash, &.{});
683 try history.putCommit(root_commit);
684 _ = try history.createBranch("main", root_commit.hash);
685
686 var database_session = DatabaseSession.initWithRoot(std.testing.allocator, try history.checkoutBranch("main"), &database_root_value);
687 defer database_session.deinit();
688 var workspace = try DatabaseWrite.Workspace.allocate(
689 std.testing.allocator,
690 testing_write_limits,
691 );
692 defer workspace.deallocate(std.testing.allocator);
693
694 var first_users = try RelationSession.open(std.testing.allocator, &catalog, "users");
695 var first_users_live = true;
696 errdefer if (first_users_live) first_users.deinit();
697 try first_users.put(1, &.{.{ .text = "ada" }});
698 var first_write = try database_session.beginWrite(
699 &workspace,
700 std.testing.allocator,
701 testing_write_limits,
702 .{ .durability = .buffered },
703 );
704 defer first_write.deinit();
705 try first_write.stageRelation(&first_users);
706 first_users_live = false;
707 var first_flush = try first_write.flush();
708 defer first_flush.deinit();
709
710 const first_items_hash = testEntryHash(database_session.workingRoot(), "items");
711 var first_items = try RelationSession.open(std.testing.allocator, &catalog, "items");
712 defer first_items.deinit();
713 try std.testing.expectEqual(@as(usize, 0), first_items.root.table.summary.entries);
714
715 var live_items = try catalog.openRelation(std.testing.allocator, "items");
716 defer live_items.deinit();
717 _ = try live_items.relation.put(std.testing.allocator, 9, &.{.{ .text = "live-only" }}, .{ .durability = .buffered });
718 var live_root = try testingDatabaseRoot(std.testing.allocator, &catalog);
719 defer live_root.deinit();
720 try std.testing.expect(!version.same(first_items_hash, testEntryHash(&live_root, "items")));
721 var live_items_after = try RelationSession.open(std.testing.allocator, &catalog, "items");
722 defer live_items_after.deinit();
723 try std.testing.expectEqual(@as(usize, 1), live_items_after.root.table.summary.entries);
724
725 var second_users = try RelationSession.open(std.testing.allocator, &catalog, "users");
726 var second_users_live = true;
727 errdefer if (second_users_live) second_users.deinit();
728 try second_users.put(2, &.{.{ .text = "grace" }});
729 var second_write = try database_session.beginWrite(
730 &workspace,
731 std.testing.allocator,
732 testing_write_limits,
733 .{ .durability = .buffered },
734 );
735 defer second_write.deinit();
736 try second_write.stageRelation(&second_users);
737 second_users_live = false;
738 var second_flush = try second_write.flush();
739 defer second_flush.deinit();
740
741 const second_root = database_session.workingRoot();
742 try std.testing.expect(version.same(second_flush.database, second_root.hash));
743 try std.testing.expect(version.same(second_flush.onlyRelation().relation, testEntryHash(second_root, "users")));
744 try std.testing.expect(version.same(testEntryHash(second_root, "items"), first_items_hash));
745 var second_users_after = try RelationSession.open(std.testing.allocator, &catalog, "users");
746 defer second_users_after.deinit();
747 try std.testing.expectEqual(@as(usize, 2), second_users_after.root.table.summary.entries);
748 }
749
750 test "database session deinit discards queued relation edits" {
751 var tmp = std.testing.tmpDir(.{});
752 defer tmp.cleanup();
753
754 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
755 .paths = .{ .database = "database-session-discard.db", .wal = "database-session-discard.wal" },
756 .header = testingHeader(),
757 });
758 defer database.deinit();
759 try database.reserve(.{ .wal_frames = 512 });
760
761 var catalog = try catalog_mod.Catalog.open(&database, .{});
762 _ = try catalog.createRelation(std.testing.allocator, .{
763 .name = "items",
764 .columns = &.{.{ .name = "value" }},
765 }, .{ .durability = .buffered });
766
767 var database_root_value = try testingDatabaseRoot(std.testing.allocator, &catalog);
768 const database_root = database_root_value.hash;
769
770 var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "database-session-discard.history", .recovery = .reject });
771 defer history.deinit();
772 const root_commit = version.Commit.init(database_root, &.{});
773 try history.putCommit(root_commit);
774 _ = try history.createBranch("main", root_commit.hash);
775
776 var database_session = DatabaseSession.initWithRoot(std.testing.allocator, try history.checkoutBranch("main"), &database_root_value);
777 var relation_session = try RelationSession.open(std.testing.allocator, &catalog, "items");
778 var relation_session_live = true;
779 errdefer if (relation_session_live) relation_session.deinit();
780
781 try relation_session.put(11, &.{.{ .text = "discarded" }});
782 var workspace = try DatabaseWrite.Workspace.allocate(
783 std.testing.allocator,
784 testing_write_limits,
785 );
786 defer workspace.deallocate(std.testing.allocator);
787 var write = try database_session.beginWrite(
788 &workspace,
789 std.testing.allocator,
790 testing_write_limits,
791 .{ .durability = .buffered },
792 );
793 try write.stageRelation(&relation_session);
794 relation_session_live = false;
795 try std.testing.expectEqual(@as(usize, 1), write.pendingRelations());
796
797 var handle = try catalog.openRelation(std.testing.allocator, "items");
798 defer handle.deinit();
799 const missing = try handle.relation.get(std.testing.allocator, 11);
800 if (missing) |bytes| std.testing.allocator.free(bytes);
801 try std.testing.expect(missing == null);
802 try std.testing.expect(version.same(database_root, database_session.checkout.working.working));
803 database_session.deinit();
804 }
805
806 test "database session applies catalog roots before history commits" {
807 var tmp = std.testing.tmpDir(.{});
808 defer tmp.cleanup();
809
810 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
811 .paths = .{ .database = "catalog-session.db", .wal = "catalog-session.wal" },
812 .header = testingHeader(),
813 });
814 defer database.deinit();
815 try database.reserve(.{ .wal_frames = 512 });
816
817 var catalog = try catalog_mod.Catalog.open(&database, .{});
818 var database_root_value = try testingDatabaseRoot(std.testing.allocator, &catalog);
819 const initial_root = database_root_value.hash;
820
821 var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "catalog-session.history", .recovery = .reject });
822 defer history.deinit();
823 const root_commit = version.Commit.init(initial_root, &.{});
824 try history.putCommit(root_commit);
825 _ = try history.createBranch("main", root_commit.hash);
826
827 var database_session = DatabaseSession.initWithRoot(std.testing.allocator, try history.checkoutBranch("main"), &database_root_value);
828 defer database_session.deinit();
829 const columns = [_]catalog_mod.ColumnDefinition{
830 .{ .name = "name" },
831 .{ .name = "score" },
832 };
833 const indexes = [_]catalog_mod.IndexDefinition{.{
834 .name = "items_score",
835 .fields = &.{1},
836 }};
837 const create_flush = try database_session.createRelation(std.testing.allocator, &catalog, .{
838 .name = "items",
839 .columns = &columns,
840 .indexes = &indexes,
841 }, .{ .durability = .buffered });
842
843 try std.testing.expectEqual(@as(u64, 1), create_flush.commit.schema.version);
844 try std.testing.expect(version.same(create_flush.database, database_session.checkout.working.working));
845 const create_root = database_session.workingRoot();
846 try std.testing.expect(version.same(create_flush.database, create_root.hash));
847 try std.testing.expectEqual(@as(usize, 1), create_root.entries.len);
848 try std.testing.expectEqualStrings("items", create_root.entries[0].name);
849 var created_items = try RelationSession.open(std.testing.allocator, &catalog, "items");
850 defer created_items.deinit();
851 try std.testing.expectEqual(@as(usize, 0), created_items.root.table.summary.entries);
852 try std.testing.expect(database_session.checkout.working.dirty());
853 try std.testing.expect(!database_session.checkout.working.hasStaged());
854 try std.testing.expect(version.same(root_commit.hash, (try history.ref("main")).?.target));
855
856 var relation_session = try RelationSession.open(std.testing.allocator, &catalog, "items");
857 var relation_session_live = true;
858 errdefer if (relation_session_live) relation_session.deinit();
859 try relation_session.put(1, &.{ .{ .text = "ada" }, .{ .integer = 7 } });
860 var workspace = try DatabaseWrite.Workspace.allocate(
861 std.testing.allocator,
862 testing_write_limits,
863 );
864 defer workspace.deallocate(std.testing.allocator);
865 var write = try database_session.beginWrite(
866 &workspace,
867 std.testing.allocator,
868 testing_write_limits,
869 .{ .durability = .buffered },
870 );
871 defer write.deinit();
872 try write.stageRelation(&relation_session);
873 relation_session_live = false;
874 var row_database_flush = try write.flush();
875 defer row_database_flush.deinit();
876 const row_flush = row_database_flush.onlyRelation();
877
878 const analyze_flush = try database_session.analyzeRelation(std.testing.allocator, &catalog, "items", .{ .durability = .buffered });
879 try std.testing.expectEqual(create_flush.commit.schema, analyze_flush.commit.schema);
880 try std.testing.expect(!version.same(row_flush.database, analyze_flush.database));
881 try std.testing.expect(version.same(analyze_flush.database, database_session.checkout.working.working));
882 const analyze_root = database_session.workingRoot();
883 try std.testing.expect(version.same(analyze_flush.database, analyze_root.hash));
884 try std.testing.expectEqual(@as(usize, 1), analyze_root.entries.len);
885 var analyzed_items = try RelationSession.open(std.testing.allocator, &catalog, "items");
886 defer analyzed_items.deinit();
887 try std.testing.expectEqual(@as(usize, 1), analyzed_items.root.table.summary.entries);
888 try std.testing.expect(!version.same(analyzed_items.root.stats.hash, version.emptyHash("sql.stats.none")));
889 var analyzed_stats = try catalog.relationStats(std.testing.allocator, "items");
890 defer if (analyzed_stats) |*relation_stats| relation_stats.deinit();
891 try std.testing.expect(analyzed_stats != null);
892
893 database_session.stage();
894 const commit_hash = try database_session.commit(&history);
895 try std.testing.expect(version.same(commit_hash, (try history.ref("main")).?.target));
896 try std.testing.expect(version.same(analyze_flush.database, database_session.checkout.working.base));
897 try std.testing.expect(version.same(analyze_flush.database, database_session.workingRoot().hash));
898 try std.testing.expect(!database_session.checkout.working.dirty());
899
900 const checkout = try history.checkoutBranch("main");
901 try std.testing.expect(version.same(commit_hash, checkout.head));
902 try std.testing.expect(version.same(analyze_flush.database, checkout.working.base));
903 }
904
905 test "database session clears analyzed stats after relation edits" {
906 var tmp = std.testing.tmpDir(.{});
907 defer tmp.cleanup();
908
909 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
910 .paths = .{ .database = "catalog-session-stats.db", .wal = "catalog-session-stats.wal" },
911 .header = testingHeader(),
912 });
913 defer database.deinit();
914 try database.reserve(.{ .wal_frames = 512 });
915
916 var catalog = try catalog_mod.Catalog.open(&database, .{});
917 _ = try catalog.createRelation(std.testing.allocator, .{
918 .name = "items",
919 .columns = &.{.{ .name = "name" }},
920 }, .{ .durability = .buffered });
921
922 var initial_root = try testingDatabaseRoot(std.testing.allocator, &catalog);
923 const initial_commit = version.Commit.init(initial_root.hash, &.{});
924 const initial_root_hash = initial_root.hash;
925 var database_session = DatabaseSession.initWithRoot(std.testing.allocator, branch.checkout(.{
926 .name = "main",
927 .target = initial_commit.hash,
928 }, initial_root_hash), &initial_root);
929 defer database_session.deinit();
930 var workspace = try DatabaseWrite.Workspace.allocate(
931 std.testing.allocator,
932 testing_write_limits,
933 );
934 defer workspace.deallocate(std.testing.allocator);
935
936 var first_relation = try RelationSession.open(std.testing.allocator, &catalog, "items");
937 var first_relation_live = true;
938 errdefer if (first_relation_live) first_relation.deinit();
939 try first_relation.put(1, &.{.{ .text = "one" }});
940 var first_write = try database_session.beginWrite(
941 &workspace,
942 std.testing.allocator,
943 testing_write_limits,
944 .{ .durability = .buffered },
945 );
946 defer first_write.deinit();
947 try first_write.stageRelation(&first_relation);
948 first_relation_live = false;
949 var first_flush = try first_write.flush();
950 defer first_flush.deinit();
951
952 const analyze_flush = try database_session.analyzeRelation(std.testing.allocator, &catalog, "items", .{ .durability = .buffered });
953 var analyzed_relation = try RelationSession.open(std.testing.allocator, &catalog, "items");
954 defer analyzed_relation.deinit();
955 try std.testing.expect(!version.same(analyzed_relation.root.stats.hash, version.emptyHash("sql.stats.none")));
956 try std.testing.expect(version.same(testEntryHash(database_session.workingRoot(), "items"), analyzed_relation.root.hash));
957 var analyzed_stats = (try catalog.relationStats(std.testing.allocator, "items")).?;
958 analyzed_stats.deinit();
959
960 var second_relation = try RelationSession.open(std.testing.allocator, &catalog, "items");
961 var second_relation_live = true;
962 errdefer if (second_relation_live) second_relation.deinit();
963 try second_relation.put(2, &.{.{ .text = "two" }});
964 var second_write = try database_session.beginWrite(
965 &workspace,
966 std.testing.allocator,
967 testing_write_limits,
968 .{ .durability = .buffered },
969 );
970 defer second_write.deinit();
971 try second_write.stageRelation(&second_relation);
972 second_relation_live = false;
973 var second_flush = try second_write.flush();
974 defer second_flush.deinit();
975
976 try std.testing.expect(!version.same(analyze_flush.database, second_flush.onlyRelation().database));
977 var edited_relation = try RelationSession.open(std.testing.allocator, &catalog, "items");
978 defer edited_relation.deinit();
979 try std.testing.expect(version.same(edited_relation.root.stats.hash, version.emptyHash("sql.stats.none")));
980 try std.testing.expect(version.same(testEntryHash(database_session.workingRoot(), "items"), edited_relation.root.hash));
981 try std.testing.expect((try catalog.relationStats(std.testing.allocator, "items")) == null);
982 try std.testing.expectEqual(@as(usize, 2), edited_relation.root.table.summary.entries);
983 }
984
985 fn testEntryHash(root: *const version.DatabaseRoot, name: []const u8) version.Hash {
986 for (root.entries) |entry| {
987 if (std.mem.eql(u8, entry.name, name)) return entry.hash;
988 }
989 unreachable;
990 }
991
992 fn testingHeader() wal.Header {
993 return .{
994 .sequence = 2101,
995 .salt = .{ .first = 0xabcd_0303, .second = 0xdcba_0404 },
996 };
997 }