feat: flesh out query

This commit is contained in:
dualfroz committed 2026-09-09 12:30:05 +02:00
1 parent ca52ad70f7
commit c4c651f782
1 file changed
+159
+159
View File
@@ -2065,3 +2065,162 @@ test "selectPageOrdered with an empty order-by falls through to selectPage's ear
for (ordered, plain) |o, p| try std.testing.expectEqual(o.get(pkField).?.int, p.get(pkField).?.int);
}
test "createCompositeIndex backfills then full-tuple eq select uses it" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
var table = try Table.open(&db, allocator, "users");
defer table.deinit();
try table.put(.{ .int = 1 }, &.{
.{ .name = "city", .value = .{ .text = "warsaw" } },
.{ .name = "age", .value = .{ .int = 30 } },
});
try table.put(.{ .int = 2 }, &.{
.{ .name = "city", .value = .{ .text = "warsaw" } },
.{ .name = "age", .value = .{ .int = 40 } },
});
try table.put(.{ .int = 3 }, &.{
.{ .name = "city", .value = .{ .text = "krakow" } },
.{ .name = "age", .value = .{ .int = 30 } },
});
try table.createCompositeIndex("city_age", &.{ "city", "age" });
try std.testing.expect(table.hasCompositeIndex("city_age"));
const preds = [_]Predicate{
.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } },
.{ .eq = .{ .field = "age", .value = .{ .int = 30 } } },
};
const results = try table.select(.{ .and_ = &preds });
defer freeRows(allocator, results);
try std.testing.expectEqual(@as(usize, 1), results.len);
try std.testing.expectEqual(@as(i64, 1), results[0].get(pkField).?.int);
}
test "composite index leftmost prefix narrows without constraining every column" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
var table = try Table.open(&db, allocator, "users");
defer table.deinit();
try table.createCompositeIndex("city_age", &.{ "city", "age" });
try table.put(.{ .int = 1 }, &.{
.{ .name = "city", .value = .{ .text = "warsaw" } },
.{ .name = "age", .value = .{ .int = 30 } },
});
try table.put(.{ .int = 2 }, &.{
.{ .name = "city", .value = .{ .text = "warsaw" } },
.{ .name = "age", .value = .{ .int = 40 } },
});
try table.put(.{ .int = 3 }, &.{
.{ .name = "city", .value = .{ .text = "krakow" } },
.{ .name = "age", .value = .{ .int = 50 } },
});
// Only the leading column is constrained - the composite index must
// still narrow the scan to the two warsaw rows via its leftmost prefix,
// and the (unindexed-alone) age column is unconstrained here.
const results = try table.select(.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } });
defer freeRows(allocator, results);
try std.testing.expectEqual(@as(usize, 2), results.len);
}
test "composite index uses a range bound on the column after the matched prefix" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
var table = try Table.open(&db, allocator, "users");
defer table.deinit();
try table.createCompositeIndex("city_age", &.{ "city", "age" });
var age: i64 = 20;
while (age < 30) : (age += 1) {
try table.put(.{ .int = age }, &.{
.{ .name = "city", .value = .{ .text = "warsaw" } },
.{ .name = "age", .value = .{ .int = age } },
});
try table.put(.{ .int = age + 100 }, &.{
.{ .name = "city", .value = .{ .text = "krakow" } },
.{ .name = "age", .value = .{ .int = age } },
});
}
// city == "warsaw" and 23 <= age < 27: must stay inside the warsaw
// bucket of the composite index even though krakow rows overlap the
// same age range byte-for-byte.
const preds = [_]Predicate{
.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } },
.{ .gte = .{ .field = "age", .value = .{ .int = 23 } } },
.{ .lt = .{ .field = "age", .value = .{ .int = 27 } } },
};
const results = try table.select(.{ .and_ = &preds });
defer freeRows(allocator, results);
try std.testing.expectEqual(@as(usize, 4), results.len);
for (results) |r| {
try std.testing.expectEqualStrings("warsaw", r.get("city").?.text);
try std.testing.expect(r.get("age").?.int >= 23 and r.get("age").?.int < 27);
}
// A one-sided range (age > 27) narrows the same way.
const openPreds = [_]Predicate{
.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } },
.{ .gt = .{ .field = "age", .value = .{ .int = 27 } } },
};
const openResults = try table.select(.{ .and_ = &openPreds });
defer freeRows(allocator, openResults);
try std.testing.expectEqual(@as(usize, 2), openResults.len);
for (openResults) |r| {
try std.testing.expectEqualStrings("warsaw", r.get("city").?.text);
try std.testing.expect(r.get("age").?.int > 27);
}
}
test "composite index stays in sync across update and delete" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
var table = try Table.open(&db, allocator, "users");
defer table.deinit();
try table.createCompositeIndex("city_age", &.{ "city", "age" });
const preds = [_]Predicate{
.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } },
.{ .eq = .{ .field = "age", .value = .{ .int = 30 } } },
};
const pred = Predicate{ .and_ = &preds };
try table.put(.{ .int = 1 }, &.{
.{ .name = "city", .value = .{ .text = "warsaw" } },
.{ .name = "age", .value = .{ .int = 30 } },
});
{
const found = try table.select(pred);
defer freeRows(allocator, found);
try std.testing.expectEqual(@as(usize, 1), found.len);
}
// Moving age away from 30 must drop the old composite entry.
try table.put(.{ .int = 1 }, &.{
.{ .name = "city", .value = .{ .text = "warsaw" } },
.{ .name = "age", .value = .{ .int = 31 } },
});
{
const stale = try table.select(pred);
defer freeRows(allocator, stale);
try std.testing.expectEqual(@as(usize, 0), stale.len);
}