lib/sql/src/table.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const file = @import("file.zig");
4 const key = @import("key.zig");
5 const page = @import("page.zig");
6 const row = @import("row.zig");
7 const trace = @import("trace.zig");
8 const tree = @import("tree.zig");
9 const wal = @import("wal.zig");
10
11 const Allocator = std.mem.Allocator;
12 const reader_get_instruction_page_alignment = 4096;
13
14 pub const Error = tree.Error || row.Error || key.Error;
15
16 pub const ValueLimits = struct {
17 max_value_bytes: usize = 0,
18
19 pub fn inspect(table: *const Table, rowid: i64) Error!?ValueLimits {
20 const value_bytes = (try table.valueLength(rowid)) orelse return null;
21 return .{ .max_value_bytes = value_bytes };
22 }
23
24 pub fn include(self: *ValueLimits, limits: ValueLimits) void {
25 self.max_value_bytes = @max(self.max_value_bytes, limits.max_value_bytes);
26 }
27 };
28
29 pub const ValueCapacity = struct {
30 max_value_bytes: usize,
31 storage_bytes: usize,
32
33 pub fn derive(limits: ValueLimits) ValueCapacity {
34 return .{
35 .max_value_bytes = limits.max_value_bytes,
36 .storage_bytes = limits.max_value_bytes,
37 };
38 }
39 };
40
41 pub const ValueStorage = struct {
42 phase: alloc_phase.capacity.Phase,
43 capacity: ValueCapacity,
44 bytes: []u8,
45
46 pub const Limits: type = ValueLimits;
47 pub const Capacity: type = ValueCapacity;
48 pub const Exhaustion = error{ValueCapacityExceeded};
49 pub const InitError: type = Allocator.Error;
50
51 pub const claim: alloc_phase.capacity.Declaration = .{
52 .source = .{
53 .id = "sql.table_value_storage",
54 .kind = .phase_static,
55 .limit_source = .caller,
56 .storage = .{
57 .covered = &.{
58 .{
59 .id = "one_reusable_encoded_row_value_region",
60 .lifetime = .steady,
61 .detail = "one reusable encoded row value region",
62 },
63 },
64 .excluded = &.{
65 "tree traversal, database/WAL bytes, file handles, and operating-system page cache",
66 "file read cache, pager history, search hit storage, and decoded result fields",
67 "caller-owned keys and trace instrumentation",
68 },
69 },
70 .capacity = .{
71 .inputs = &.{
72 alloc_phase.capacity.bindInput(Limits, "max_value_bytes", "max_value_bytes"),
73 },
74 .type_selectors = &.{},
75 .nodes = &.{
76 .{ .input = 0 },
77 },
78 .assertions = &.{.{
79 .scope = .closure_total,
80 .measure = .retained,
81 .relation = .exact,
82 .expression = 0,
83 }},
84 },
85 .overload = .{
86 .kind = .reject_before_mutation,
87 .detail = "oversize values return ValueCapacityExceeded before reusable bytes mutate",
88 },
89 .risks = .{
90 .transitive = .{
91 .status = .open,
92 .detail = "tree reads use separate file, pager, cache, and trace owners",
93 },
94 .foreign = .{
95 .status = .open,
96 .detail = "base-page reads may enter the host filesystem and OS cache",
97 },
98 },
99 .obligations = &.{
100 .{ .key = "sql_table_value_capacity", .role = .capacity_model },
101 .{ .key = "sql_table_value_oom_retry", .role = .custom },
102 .{ .key = "sql_table_value_sealed", .role = .overload },
103 .{ .key = "sql_table_value_semantics", .role = .custom },
104 },
105 },
106 .bindings = .{
107 .owner = @This(),
108 .seal = .{
109 .family = alloc_phase.capacity.selector(@This().activate),
110 .premise = .{
111 .class = .checked_semantic_fact,
112 .authority = .checker,
113 },
114 },
115 .teardown = .{
116 .family = alloc_phase.capacity.selector(@This().deinit),
117 .premise = .{
118 .class = .checked_semantic_fact,
119 .authority = .checker,
120 },
121 },
122 },
123 };
124
125 pub fn init(allocator: Allocator, limits: ValueLimits) InitError!ValueStorage {
126 const capacity = ValueCapacity.derive(limits);
127 const bytes = if (capacity.storage_bytes == 0)
128 @as([]u8, &.{})
129 else
130 try allocator.alloc(u8, capacity.storage_bytes);
131 return .{
132 .phase = .initialization,
133 .capacity = capacity,
134 .bytes = bytes,
135 };
136 }
137
138 pub fn activate(self: *ValueStorage) void {
139 std.debug.assert(self.phase == .initialization);
140 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
141 self.phase = .steady;
142 }
143
144 pub fn get(
145 self: *ValueStorage,
146 table: *const Table,
147 rowid: i64,
148 ) (Error || Exhaustion)!?[]const u8 {
149 std.debug.assert(self.phase == .steady);
150 const value = table.getInto(rowid, self.bytes) catch |err| switch (err) {
151 error.OutputTooSmall => return error.ValueCapacityExceeded,
152 else => return err,
153 };
154 return value;
155 }
156
157 pub fn deinit(self: *ValueStorage, allocator: Allocator) void {
158 std.debug.assert(self.phase != .teardown);
159 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
160 self.phase = .teardown;
161 if (self.bytes.len != 0) allocator.free(self.bytes);
162 self.bytes = &.{};
163 }
164 };
165
166 comptime {
167 alloc_phase.capacity.requireAllocatorRejectingOwnerShape(ValueStorage);
168 }
169
170 pub const Options = struct {
171 tree: tree.Options = .{},
172 };
173
174 /// Chooses what a table scan carries back for each row, so a scan then pays
175 /// only for the part of each row it will look at. `key` gives the row's key and
176 /// an empty byte slice, `record` gives the record bytes as they are stored,
177 /// which for a large row is the pointer to its overflow pages, and `value`
178 /// gives the decoded row value and reads the overflow pages when the record
179 /// points at them. The projection is chosen once for a whole scan, where `scan`
180 /// picks `value` for callers that do not say, and the type is re-exported from
181 /// the B-tree layer so a table scan and a tree scan take the same values.
182 pub const Projection = tree.Projection;
183
184 pub const Entry = struct {
185 rowid: i64,
186 bytes: []const u8,
187
188 pub fn view(self: Entry) row.Error!row.View {
189 return try row.View.init(self.bytes);
190 }
191 };
192
193 pub const Reader = struct {
194 rows: tree.Reader,
195
196 pub fn open(snapshot: file.Snapshot, options: Options) Error!Reader {
197 return .{ .rows = try tree.Reader.open(snapshot, options.tree) };
198 }
199
200 pub fn lastRowId(self: *const Reader) Error!?i64 {
201 var buffer: [key.rowid_size]u8 = undefined;
202 const last = (try self.rows.lastKey(&buffer)) orelse return null;
203 return try key.decodeRowId(last);
204 }
205
206 pub fn get(self: *const Reader, allocator: Allocator, rowid: i64) align(reader_get_instruction_page_alignment) Error!?[]u8 {
207 const phase = trace.scope("table.get");
208 defer phase.end();
209
210 var key_bytes: [key.rowid_size]u8 = undefined;
211 return try self.rows.get(allocator, try key.encodeRowId(&key_bytes, rowid));
212 }
213
214 pub fn valueLength(self: *const Reader, rowid: i64) Error!?usize {
215 var key_bytes: [key.rowid_size]u8 = undefined;
216 return try self.rows.valueLength(try key.encodeRowId(&key_bytes, rowid));
217 }
218
219 pub fn getInto(self: *const Reader, rowid: i64, target: []u8) Error!?[]u8 {
220 var key_bytes: [key.rowid_size]u8 = undefined;
221 return try self.rows.getInto(try key.encodeRowId(&key_bytes, rowid), target);
222 }
223
224 /// Starts a scan of the rows from `start` up to `end` in `target`.
225 pub fn scan(
226 self: *const Reader,
227 target: *Scan,
228 allocator: Allocator,
229 start: ?i64,
230 end: ?i64,
231 ) Error!void {
232 try self.scanProjected(target, allocator, start, end, .value);
233 }
234
235 pub fn scanProjected(
236 self: *const Reader,
237 target: *Scan,
238 allocator: Allocator,
239 start: ?i64,
240 end: ?i64,
241 projection: Projection,
242 ) Error!void {
243 const phase = trace.scope("table.scan");
244 defer phase.end();
245
246 var start_bytes: [key.rowid_size]u8 = undefined;
247 var end_bytes: [key.rowid_size]u8 = undefined;
248 const start_key = if (start) |rowid| try key.encodeRowId(&start_bytes, rowid) else null;
249 const end_key = if (end) |rowid| try key.encodeRowId(&end_bytes, rowid) else null;
250 try self.rows.scan(&target.rows, allocator, start_key, end_key, projection);
251 }
252
253 pub fn summarize(self: *const Reader) Error!tree.Summary {
254 const phase = trace.scope("table.summarize");
255 defer phase.end();
256
257 return try self.rows.summarize();
258 }
259
260 /// Returns how many rows the table holds in this reader's snapshot. A
261 /// caller sizes a scan's output by it without walking the table.
262 pub fn count(self: *const Reader) Error!usize {
263 return try self.rows.count();
264 }
265 };
266
267 pub const Table = struct {
268 rows: tree.Tree,
269
270 pub fn open(database: *file.Database, options: Options) Error!Table {
271 return .{ .rows = try tree.Tree.open(database, options.tree) };
272 }
273
274 pub fn reader(self: *const Table, snapshot: file.Snapshot) Error!Reader {
275 return .{ .rows = try self.rows.reader(snapshot) };
276 }
277
278 pub fn lastRowId(self: *const Table) Error!?i64 {
279 var read = try self.rows.database.beginRead();
280 defer read.deinit();
281 const opened = try self.reader(read.snapshot());
282 return try opened.lastRowId();
283 }
284
285 pub fn put(self: *Table, rowid: i64, values: []const row.Value, options: file.CommitOptions) Error!file.Commit {
286 const phase = trace.scope("table.put");
287 defer phase.end();
288
289 var write = try tree.Write.beginTree(&self.rows);
290 defer write.deinit();
291 try self.putIn(&write, rowid, values);
292 return try write.commit(options);
293 }
294
295 pub fn putIn(self: *Table, write: *tree.Write, rowid: i64, values: []const row.Value) Error!void {
296 var key_bytes: [key.rowid_size]u8 = undefined;
297 var row_bytes: [page.size]u8 = undefined;
298 const encoded_key = try key.encodeRowId(&key_bytes, rowid);
299 const encoded_row = try row.encode(&row_bytes, values);
300 try write.put(&self.rows, encoded_key, encoded_row);
301 }
302
303 pub fn putEncoded(self: *Table, rowid: i64, bytes: []const u8, options: file.CommitOptions) Error!file.Commit {
304 const phase = trace.scope("table.put_encoded");
305 defer phase.end();
306
307 var write = try tree.Write.beginTree(&self.rows);
308 defer write.deinit();
309 try self.putEncodedIn(&write, rowid, bytes);
310 return try write.commit(options);
311 }
312
313 pub fn putEncodedIn(self: *Table, write: *tree.Write, rowid: i64, bytes: []const u8) Error!void {
314 _ = try row.View.init(bytes);
315 var key_bytes: [key.rowid_size]u8 = undefined;
316 try write.put(&self.rows, try key.encodeRowId(&key_bytes, rowid), bytes);
317 }
318
319 pub fn get(self: *const Table, allocator: Allocator, rowid: i64) Error!?[]u8 {
320 var read = try self.rows.database.beginRead();
321 defer read.deinit();
322 const opened = try self.reader(read.snapshot());
323 return try opened.get(allocator, rowid);
324 }
325
326 pub fn valueLength(self: *const Table, rowid: i64) Error!?usize {
327 var read = try self.rows.database.beginRead();
328 defer read.deinit();
329 const opened = try self.reader(read.snapshot());
330 return try opened.valueLength(rowid);
331 }
332
333 pub fn getInto(self: *const Table, rowid: i64, target: []u8) Error!?[]u8 {
334 var read = try self.rows.database.beginRead();
335 defer read.deinit();
336 const opened = try self.reader(read.snapshot());
337 return try opened.getInto(rowid, target);
338 }
339
340 pub fn delete(self: *Table, rowid: i64, options: file.CommitOptions) Error!file.Commit {
341 const phase = trace.scope("table.delete");
342 defer phase.end();
343
344 var write = try tree.Write.beginTree(&self.rows);
345 defer write.deinit();
346 try self.deleteIn(&write, rowid);
347 return try write.commit(options);
348 }
349
350 pub fn deleteIn(self: *Table, write: *tree.Write, rowid: i64) Error!void {
351 var key_bytes: [key.rowid_size]u8 = undefined;
352 try write.delete(&self.rows, try key.encodeRowId(&key_bytes, rowid));
353 }
354
355 /// Starts a scan of the rows from `start` up to `end` in `target`.
356 pub fn scan(
357 self: *const Table,
358 target: *Scan,
359 allocator: Allocator,
360 start: ?i64,
361 end: ?i64,
362 ) Error!void {
363 try self.scanProjected(target, allocator, start, end, .value);
364 }
365
366 pub fn scanProjected(
367 self: *const Table,
368 target: *Scan,
369 allocator: Allocator,
370 start: ?i64,
371 end: ?i64,
372 projection: Projection,
373 ) Error!void {
374 var read = try self.rows.database.beginRead();
375 defer read.deinit();
376 const opened = try self.reader(read.snapshot());
377 try opened.scanProjected(target, allocator, start, end, projection);
378 }
379
380 pub fn summarize(self: *const Table) Error!tree.Summary {
381 var read = try self.rows.database.beginRead();
382 defer read.deinit();
383 const opened = try self.reader(read.snapshot());
384 return try opened.summarize();
385 }
386
387 pub fn count(self: *const Table) Error!usize {
388 var read = try self.rows.database.beginRead();
389 defer read.deinit();
390 const opened = try self.reader(read.snapshot());
391 return try opened.count();
392 }
393
394 pub fn summarizeIn(self: *const Table, write: *const tree.Write) Error!tree.Summary {
395 const phase = trace.scope("table.summarize_in");
396 defer phase.end();
397
398 return try self.rows.summarizeIn(write);
399 }
400 };
401
402 pub const Scan = struct {
403 rows: tree.Scan,
404
405 pub fn deinit(self: *Scan) void {
406 self.rows.deinit();
407 self.* = undefined;
408 }
409
410 pub fn next(self: *Scan) Error!?Entry {
411 if (try self.rows.next()) |entry| {
412 return .{
413 .rowid = try key.decodeRowId(entry.key),
414 .bytes = entry.bytes,
415 };
416 }
417 return null;
418 }
419 };
420
421 test "table value capacity matches the independent maximum model" {
422 comptime {
423 @stardustClaim(
424 @import("alloc_phase").capacity.witness(ValueStorage, "sql_table_value_capacity"),
425 null,
426 null,
427 null,
428 null,
429 null,
430 null,
431 );
432 }
433
434 for (0..4097) |value_bytes| {
435 const expected = ValueCapacity{
436 .max_value_bytes = value_bytes,
437 .storage_bytes = value_bytes,
438 };
439 try std.testing.expectEqual(
440 expected,
441 ValueCapacity.derive(.{ .max_value_bytes = value_bytes }),
442 );
443 }
444 try std.testing.expectEqual(
445 ValueCapacity{
446 .max_value_bytes = std.math.maxInt(usize),
447 .storage_bytes = std.math.maxInt(usize),
448 },
449 ValueCapacity.derive(.{ .max_value_bytes = std.math.maxInt(usize) }),
450 );
451 }
452
453 fn checkValueStorageInitFailures(allocator: Allocator) !void {
454 var storage = try ValueStorage.init(allocator, .{ .max_value_bytes = 4096 });
455 storage.deinit(allocator);
456 }
457
458 test "table value storage initialization cleans allocation failure and retries" {
459 comptime {
460 @stardustClaim(
461 @import("alloc_phase").capacity.witness(ValueStorage, "sql_table_value_oom_retry"),
462 null,
463 null,
464 null,
465 null,
466 null,
467 null,
468 );
469 }
470
471 try std.testing.checkAllAllocationFailures(
472 std.testing.allocator,
473 checkValueStorageInitFailures,
474 .{},
475 );
476
477 var storage = try ValueStorage.init(std.testing.allocator, .{ .max_value_bytes = 4096 });
478 defer storage.deinit(std.testing.allocator);
479 storage.activate();
480 try std.testing.expectEqual(alloc_phase.capacity.Phase.steady, storage.phase);
481 }
482
483 fn checkSealedValueReads(
484 table: *const Table,
485 storage: *ValueStorage,
486 rejecting: *ValueStorage,
487 phase_allocator: *alloc_phase.SealedPhaseAllocator,
488 large: []const u8,
489 ) !void {
490 const storage_pointer = storage.bytes.ptr;
491 const storage_capacity = storage.capacity;
492 phase_allocator.seal();
493 storage.activate();
494 rejecting.activate();
495
496 const inline_bytes = (try storage.get(table, 1)).?;
497 try std.testing.expect(storage.bytes.ptr == storage_pointer);
498 try std.testing.expectEqual(storage_capacity, storage.capacity);
499 try std.testing.expectEqualStrings(
500 "inline",
501 (try (try row.View.init(inline_bytes)).column(0)).text,
502 );
503
504 const overflow_bytes = (try storage.get(table, 2)).?;
505 try std.testing.expect(storage.bytes.ptr == storage_pointer);
506 try std.testing.expectEqual(storage_capacity, storage.capacity);
507 try std.testing.expectEqualSlices(
508 u8,
509 large,
510 (try (try row.View.init(overflow_bytes)).column(0)).blob,
511 );
512 try std.testing.expect((try storage.get(table, 3)) == null);
513
514 @memset(rejecting.bytes, 0xa5);
515 try std.testing.expectError(error.ValueCapacityExceeded, rejecting.get(table, 2));
516 for (rejecting.bytes) |byte| try std.testing.expectEqual(@as(u8, 0xa5), byte);
517 }
518
519 test "table value storage is sealed before inline overflow and rejection reads" {
520 comptime {
521 @stardustClaim(
522 @import("alloc_phase").capacity.witness(ValueStorage, "sql_table_value_sealed"),
523 null,
524 null,
525 null,
526 null,
527 null,
528 null,
529 );
530 }
531
532 var tmp = std.testing.tmpDir(.{});
533 defer tmp.cleanup();
534
535 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
536 .paths = .{ .database = "table-value-sealed.db", .wal = "table-value-sealed.wal" },
537 .header = testingHeader(),
538 });
539 defer database.deinit();
540 try database.reserve(.{ .wal_frames = 64 });
541
542 var table = try Table.open(&database, .{});
543 _ = try table.put(1, &.{.{ .text = "inline" }}, .{ .durability = .buffered });
544 var large: [page.overflow_capacity + 37]u8 = undefined;
545 for (&large, 0..) |*byte, index| byte.* = @intCast(index % 251);
546 var encoded_large: [large.len + 32]u8 = undefined;
547 _ = try table.putEncoded(
548 2,
549 try row.encode(&encoded_large, &.{.{ .blob = &large }}),
550 .{ .durability = .buffered },
551 );
552
553 const inline_limits = (try ValueLimits.inspect(&table, 1)).?;
554 var combined_limits = inline_limits;
555 combined_limits.include((try ValueLimits.inspect(&table, 2)).?);
556 try std.testing.expect((try ValueLimits.inspect(&table, 3)) == null);
557
558 var phase_allocator = try alloc_phase.SealedPhaseAllocator.init(std.testing.allocator);
559 var storage = ValueStorage.init(
560 phase_allocator.initializationAllocator(),
561 combined_limits,
562 ) catch |err| {
563 phase_allocator.abortInitialization();
564 phase_allocator.deinit();
565 return err;
566 };
567 var rejecting = ValueStorage.init(
568 phase_allocator.initializationAllocator(),
569 inline_limits,
570 ) catch |err| {
571 storage.deinit(phase_allocator.initializationAllocator());
572 phase_allocator.abortInitialization();
573 phase_allocator.deinit();
574 return err;
575 };
576 defer {
577 if (phase_allocator.phase() == .initialization) phase_allocator.abortInitialization();
578 if (phase_allocator.phase() == .steady) phase_allocator.beginTeardown();
579 if (rejecting.phase != .teardown) rejecting.deinit(phase_allocator.teardownAllocator());
580 if (storage.phase != .teardown) storage.deinit(phase_allocator.teardownAllocator());
581 phase_allocator.deinit();
582 }
583
584 try checkSealedValueReads(&table, &storage, &rejecting, &phase_allocator, &large);
585 }
586
587 test "table value storage preserves typed row semantics" {
588 comptime {
589 @stardustClaim(
590 @import("alloc_phase").capacity.witness(ValueStorage, "sql_table_value_semantics"),
591 null,
592 null,
593 null,
594 null,
595 null,
596 null,
597 );
598 }
599
600 var tmp = std.testing.tmpDir(.{});
601 defer tmp.cleanup();
602
603 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
604 .paths = .{ .database = "table-value-semantics.db", .wal = "table-value-semantics.wal" },
605 .header = testingHeader(),
606 });
607 defer database.deinit();
608 try database.reserve(.{ .wal_frames = 64 });
609
610 var table = try Table.open(&database, .{});
611 _ = try table.put(
612 7,
613 &.{ .{ .integer = -42 }, .{ .text = "placed" }, .nil },
614 .{ .durability = .buffered },
615 );
616 const limits = (try ValueLimits.inspect(&table, 7)).?;
617 var storage = try ValueStorage.init(std.testing.allocator, limits);
618 defer storage.deinit(std.testing.allocator);
619 storage.activate();
620 const view = try row.View.init((try storage.get(&table, 7)).?);
621 try std.testing.expectEqual(@as(i64, -42), (try view.column(0)).integer);
622 try std.testing.expectEqualStrings("placed", (try view.column(1)).text);
623 try std.testing.expectEqual(row.Storage.nil, std.meta.activeTag(try view.column(2)));
624 }
625
626 test "rowid table stores typed rows and scans in rowid order" {
627 var tmp = std.testing.tmpDir(.{});
628 defer tmp.cleanup();
629
630 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
631 .paths = .{ .database = "table.db", .wal = "table.wal" },
632 .header = testingHeader(),
633 });
634 defer database.deinit();
635 try database.reserve(.{ .wal_frames = 64 });
636
637 var table = try Table.open(&database, .{});
638 _ = try table.put(7, &.{ .{ .integer = 7 }, .{ .text = "seven" } }, .{ .durability = .buffered });
639 _ = try table.put(-2, &.{ .{ .integer = -2 }, .{ .text = "minus-two" } }, .{ .durability = .buffered });
640 _ = try table.put(0, &.{ .{ .integer = 0 }, .{ .text = "zero" } }, .{ .durability = .buffered });
641
642 const found = (try table.get(std.testing.allocator, -2)).?;
643 defer std.testing.allocator.free(found);
644 const view = try row.View.init(found);
645 try std.testing.expectEqual(@as(i64, -2), (try view.column(0)).integer);
646 try std.testing.expectEqualStrings("minus-two", (try view.column(1)).text);
647
648 var scan: Scan = undefined;
649 try table.scan(&scan, std.testing.allocator, null, null);
650 defer scan.deinit();
651 const first = (try scan.next()).?;
652 const second = (try scan.next()).?;
653 const third = (try scan.next()).?;
654 try std.testing.expectEqual(@as(i64, -2), first.rowid);
655 try std.testing.expectEqual(@as(i64, 0), second.rowid);
656 try std.testing.expectEqual(@as(i64, 7), third.rowid);
657 try std.testing.expect(try scan.next() == null);
658 }
659
660 test "rowid table key projection avoids overflow materialization" {
661 var tmp = std.testing.tmpDir(.{});
662 defer tmp.cleanup();
663
664 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
665 .paths = .{ .database = "table-projection.db", .wal = "table-projection.wal" },
666 .header = testingHeader(),
667 });
668 defer database.deinit();
669 try database.reserve(.{ .wal_frames = 64 });
670
671 var table = try Table.open(&database, .{});
672 const large: [2048]u8 = @splat(0xa5);
673 _ = try table.put(
674 7,
675 &.{.{ .blob = &large }},
676 .{ .durability = .buffered },
677 );
678
679 var key_failing = std.testing.FailingAllocator.init(
680 std.testing.allocator,
681 .{ .fail_index = 0 },
682 );
683 var key_scan: Scan = undefined;
684 try table.scanProjected(
685 &key_scan,
686 key_failing.allocator(),
687 null,
688 null,
689 .key,
690 );
691 defer key_scan.deinit();
692 const entry = (try key_scan.next()).?;
693 try std.testing.expectEqual(@as(i64, 7), entry.rowid);
694 try std.testing.expectEqual(@as(usize, 0), entry.bytes.len);
695 try std.testing.expect(try key_scan.next() == null);
696
697 var value_failing = std.testing.FailingAllocator.init(
698 std.testing.allocator,
699 .{ .fail_index = 0 },
700 );
701 var value_scan: Scan = undefined;
702 try table.scanProjected(
703 &value_scan,
704 value_failing.allocator(),
705 null,
706 null,
707 .value,
708 );
709 defer value_scan.deinit();
710 try std.testing.expectError(error.OutOfMemory, value_scan.next());
711 }
712
713 test "rowid table reports the last assigned rowid" {
714 var tmp = std.testing.tmpDir(.{});
715 defer tmp.cleanup();
716
717 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
718 .paths = .{ .database = "table.db", .wal = "table.wal" },
719 .header = testingHeader(),
720 });
721 defer database.deinit();
722 try database.reserve(.{ .wal_frames = 256 });
723
724 var table = try Table.open(&database, .{});
725 try std.testing.expectEqual(@as(?i64, null), try table.lastRowId());
726 _ = try table.put(7, &.{ .{ .integer = 7 }, .{ .text = "seven" } }, .{ .durability = .buffered });
727 try std.testing.expectEqual(@as(?i64, 7), try table.lastRowId());
728 _ = try table.put(3, &.{ .{ .integer = 3 }, .{ .text = "three" } }, .{ .durability = .buffered });
729 try std.testing.expectEqual(@as(?i64, 7), try table.lastRowId());
730 var rowid: i64 = 8;
731 while (rowid <= 600) : (rowid += 1) {
732 _ = try table.put(rowid, &.{ .{ .integer = rowid }, .{ .text = "filler" } }, .{ .durability = .buffered });
733 }
734 try std.testing.expectEqual(@as(?i64, 600), try table.lastRowId());
735 }
736
737 test "rowid table deletes and recovers after reopen" {
738 var tmp = std.testing.tmpDir(.{});
739 defer tmp.cleanup();
740
741 {
742 var database = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
743 .paths = .{ .database = "table.db", .wal = "table.wal" },
744 .header = testingHeader(),
745 });
746 defer database.deinit();
747 try database.reserve(.{ .wal_frames = 64 });
748
749 var table = try Table.open(&database, .{});
750 _ = try table.put(1, &.{.{ .text = "one" }}, .{ .durability = .buffered });
751 _ = try table.put(2, &.{.{ .text = "two" }}, .{ .durability = .buffered });
752 _ = try table.delete(1, .{ .durability = .buffered });
753 try database.syncWal();
754 }
755
756 var reopened = try file.Database.openForTesting(std.testing.allocator, tmp.dir, .{
757 .paths = .{ .database = "table.db", .wal = "table.wal" },
758 .header = recoveredHeader(),
759 });
760 defer reopened.deinit();
761
762 var table = try Table.open(&reopened, .{});
763 const missing = try table.get(std.testing.allocator, 1);
764 if (missing) |bytes| std.testing.allocator.free(bytes);
765 try std.testing.expect(missing == null);
766
767 const found = (try table.get(std.testing.allocator, 2)).?;
768 defer std.testing.allocator.free(found);
769 const view = try row.View.init(found);
770 try std.testing.expectEqualStrings("two", (try view.column(0)).text);
771 }
772
773 fn testingHeader() wal.Header {
774 return .{
775 .sequence = 601,
776 .salt = .{ .first = 0x7171_6262, .second = 0x5353_4444 },
777 };
778 }
779
780 fn recoveredHeader() wal.Header {
781 return .{
782 .sequence = 602,
783 .salt = .{ .first = 0x8888_9999, .second = 0xaaaa_bbbb },
784 };
785 }