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 }