lib/sql/src/properties/index.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const hypothesis = @import("hypothesis");
3 const sql = @import("sql");
4
5 const Index = sql.Index;
6
7 const key_count = 10;
8
9 const Model = struct {
10 values: [key_count]?i64,
11 };
12
13 const ModelEntry = struct {
14 value: i64,
15 rowid: i64,
16 };
17
18 pub fn settings() hypothesis.Settings {
19 return hypothesis.Settings.quick()
20 .withSeed(0x71DB_2026)
21 .withDatabase("zig-out/hypothesis-failures/sql");
22 }
23
24 fn drawUsize(conjecture: *hypothesis.ConjectureData, min: usize, max: usize, shrink_towards: usize) !usize {
25 return @intCast(try conjecture.drawInteger(
26 @intCast(min),
27 @intCast(max),
28 @intCast(shrink_towards),
29 ));
30 }
31
32 fn tableRowId(index: usize) i64 {
33 return @as(i64, @intCast(index)) - @as(i64, key_count / 2);
34 }
35
36 fn expectIndex(allocator: std.mem.Allocator, index: *const Index, model: Model) !void {
37 var expected: [key_count]ModelEntry = undefined;
38 var expected_count: usize = 0;
39 var slot: usize = 0;
40 while (slot < key_count) : (slot += 1) {
41 if (model.values[slot]) |value| {
42 expected[expected_count] = .{
43 .value = value,
44 .rowid = tableRowId(slot),
45 };
46 expected_count += 1;
47 }
48 }
49 std.mem.sort(ModelEntry, expected[0..expected_count], {}, modelLess);
50
51 var scan: sql.IndexScan = undefined;
52 try index.scan(&scan, allocator, null, null);
53 defer scan.deinit();
54 var seen: usize = 0;
55 while (try scan.next()) |entry| {
56 try std.testing.expect(seen < expected_count);
57 try std.testing.expectEqual(expected[seen].rowid, entry.rowid);
58 seen += 1;
59 }
60 try std.testing.expectEqual(expected_count, seen);
61
62 slot = 0;
63 while (slot < key_count) : (slot += 1) {
64 const value = model.values[slot] orelse continue;
65 var lookup: sql.IndexScan = undefined;
66 try index.lookup(&lookup, allocator, &.{.{ .integer = value }});
67 defer lookup.deinit();
68 var previous: ?i64 = null;
69 var matches: usize = 0;
70 while (try lookup.next()) |entry| {
71 if (previous) |rowid| try std.testing.expect(rowid < entry.rowid);
72 previous = entry.rowid;
73 const row_slot_value = entry.rowid + @as(i64, key_count / 2);
74 try std.testing.expect(row_slot_value >= 0);
75 const row_slot: usize = @intCast(row_slot_value);
76 try std.testing.expect(row_slot < key_count);
77 try std.testing.expect(model.values[row_slot] != null);
78 try std.testing.expectEqual(value, model.values[row_slot].?);
79 matches += 1;
80 }
81 try std.testing.expect(matches > 0);
82 }
83 }
84
85 fn modelLess(_: void, left: ModelEntry, right: ModelEntry) bool {
86 if (left.value == right.value) return left.rowid < right.rowid;
87 return left.value < right.value;
88 }
89
90 pub const SecondaryProperty = struct {
91 pub fn property(conjecture: *hypothesis.ConjectureData, allocator: std.mem.Allocator) !void {
92 var tmp = std.testing.tmpDir(.{});
93 defer tmp.cleanup();
94
95 var database = try sql.FileDatabase.openForTesting(allocator, tmp.dir, .{
96 .paths = .{ .database = "index.db", .wal = "index.wal" },
97 .header = .{
98 .sequence = 801,
99 .salt = .{ .first = 0x1357_cccc, .second = 0x2468_dddd },
100 },
101 });
102 var database_live = true;
103 defer if (database_live) database.deinit();
104 try database.reserve(.{ .wal_frames = 160 });
105
106 var index = try Index.open(&database, .{ .tree = .{ .meta_page = 3, .root_page = 4 } });
107 var model = Model{ .values = @as([key_count]?i64, @splat(null)) };
108
109 const steps = try drawUsize(conjecture, 1, 60, 12);
110 var step: usize = 0;
111 while (step < steps) : (step += 1) {
112 const op = try drawUsize(conjecture, 0, 99, 0);
113 const slot = try drawUsize(conjecture, 0, key_count - 1, 0);
114 const rowid = tableRowId(slot);
115 if (op < 52) {
116 const value = @as(i64, @intCast(try conjecture.drawInteger(0, 12, 0))) - 6;
117 if (model.values[slot]) |old| {
118 _ = try index.delete(rowid, &.{.{ .integer = old }}, .{ .durability = .buffered });
119 }
120 _ = try index.put(rowid, &.{.{ .integer = value }}, .{ .durability = .buffered });
121 model.values[slot] = value;
122 } else if (op < 72) {
123 if (model.values[slot]) |old| {
124 _ = try index.delete(rowid, &.{.{ .integer = old }}, .{ .durability = .buffered });
125 model.values[slot] = null;
126 } else {
127 try std.testing.expectError(error.KeyNotFound, index.delete(rowid, &.{.{ .integer = 0 }}, .{ .durability = .buffered }));
128 }
129 } else if (op < 86) {
130 try expectIndex(allocator, &index, model);
131 } else {
132 try database.syncWal();
133 database.deinit();
134 database_live = false;
135 database = try sql.FileDatabase.openForTesting(allocator, tmp.dir, .{
136 .paths = .{ .database = "index.db", .wal = "index.wal" },
137 .header = .{
138 .sequence = @intCast(802 + step),
139 .salt = .{ .first = 0xaaaa_1357, .second = 0xbbbb_2468 },
140 },
141 });
142 database_live = true;
143 try database.reserve(.{ .wal_frames = 160 });
144 index = try Index.open(&database, .{ .tree = .{ .meta_page = 3, .root_page = 4 } });
145 }
146
147 try expectIndex(allocator, &index, model);
148 }
149 }
150 };
151
152 test "property: secondary index matches the bounded ordered model across reopen" {
153 if (!@import("sql_test_options").run_property_tests) return error.SkipZigTest;
154
155 try hypothesis.checkNamed(SecondaryProperty, "sql-secondary-index", settings());
156 }