feat: expand collections

This commit is contained in:
dualfroz committed 2026-09-02 16:08:08 +02:00
1 parent c281644719
commit 8ad8dd3e8c
1 file changed
+159
+159
View File
@@ -1262,3 +1262,162 @@ test "list range supports negative indices redis style" {
try std.testing.expectEqual(@as(usize, 5), clamped.len); try std.testing.expectEqual(@as(usize, 5), clamped.len);
} }
test "zset add score incrBy remove rank" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
try ZSet.add(&db, allocator, "leaderboard", "alice", 10);
try ZSet.add(&db, allocator, "leaderboard", "bob", 20);
try ZSet.add(&db, allocator, "leaderboard", "carol", 5);
try std.testing.expectEqual(@as(f64, 10), (try ZSet.score(&db, allocator, "leaderboard", "alice")).?);
try std.testing.expectEqual(@as(usize, 3), try ZSet.card(&db, allocator, "leaderboard"));
const newScore = try ZSet.incrBy(&db, allocator, "leaderboard", "alice", 100);
try std.testing.expectEqual(@as(f64, 110), newScore);
// Ranks ascending by score: carol(5), bob(20), alice(110).
try std.testing.expectEqual(@as(usize, 0), (try ZSet.rank(&db, allocator, "leaderboard", "carol")).?);
try std.testing.expectEqual(@as(usize, 2), (try ZSet.rank(&db, allocator, "leaderboard", "alice")).?);
try std.testing.expect(try ZSet.rank(&db, allocator, "leaderboard", "missing") == null);
try std.testing.expect(try ZSet.remove(&db, allocator, "leaderboard", "bob"));
try std.testing.expect(!try ZSet.remove(&db, allocator, "leaderboard", "bob"));
try std.testing.expectEqual(@as(usize, 2), try ZSet.card(&db, allocator, "leaderboard"));
}
test "zset rangeByRank and rangeByScore return ascending order" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
try ZSet.add(&db, allocator, "z", "a", -5.5);
try ZSet.add(&db, allocator, "z", "b", 0);
try ZSet.add(&db, allocator, "z", "c", 3.25);
try ZSet.add(&db, allocator, "z", "d", 100);
const byRank = try ZSet.rangeByRank(&db, allocator, "z", 0, -1);
defer {
for (byRank) |m| m.free(allocator);
allocator.free(byRank);
}
try std.testing.expectEqual(@as(usize, 4), byRank.len);
try std.testing.expectEqualStrings("a", byRank[0].member);
try std.testing.expectEqualStrings("d", byRank[3].member);
const top2 = try ZSet.rangeByRank(&db, allocator, "z", -2, -1);
defer {
for (top2) |m| m.free(allocator);
allocator.free(top2);
}
try std.testing.expectEqual(@as(usize, 2), top2.len);
try std.testing.expectEqualStrings("c", top2[0].member);
try std.testing.expectEqualStrings("d", top2[1].member);
const byScore = try ZSet.rangeByScore(&db, allocator, "z", -1.0, 50.0);
defer {
for (byScore) |m| m.free(allocator);
allocator.free(byScore);
}
try std.testing.expectEqual(@as(usize, 2), byScore.len);
try std.testing.expectEqualStrings("b", byScore[0].member);
try std.testing.expectEqualStrings("c", byScore[1].member);
}
test "Kind.parse accepts the five CLI names and rejects the rest" {
try std.testing.expectEqual(Kind.counter, Kind.parse("counter").?);
try std.testing.expectEqual(Kind.hash, Kind.parse("hash").?);
try std.testing.expectEqual(Kind.set, Kind.parse("set").?);
try std.testing.expectEqual(Kind.list, Kind.parse("list").?);
try std.testing.expectEqual(Kind.zset, Kind.parse("zset").?);
try std.testing.expect(Kind.parse("bogus") == null);
}
test "Kind.fromTagByte is the exact inverse of tagByte for all five kinds" {
const kinds = [_]Kind{ .counter, .hash, .set, .list, .zset };
for (kinds) |k| {
try std.testing.expectEqual(k, Kind.fromTagByte(k.tagByte()).?);
}
try std.testing.expect(Kind.fromTagByte('?') == null);
}
test "exists and drop cover counter (single key, no trailing separator)" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
try std.testing.expect(!try collectionExists(&db, allocator, .counter, "hits"));
_ = try Counter.incrBy(&db, allocator, "hits", 0);
try std.testing.expect(try collectionExists(&db, allocator, .counter, "hits"));
// A same-prefixed name must not be swept up by the drop of "hits" - the
// counter key has no trailing separator, so this is the one case where a
// naive prefix scan (instead of an exact key) would over-delete.
_ = try Counter.incrBy(&db, allocator, "hitsExtra", 7);
try std.testing.expectEqual(@as(usize, 1), try dropCollection(&db, allocator, .counter, "hits"));
try std.testing.expect(!try collectionExists(&db, allocator, .counter, "hits"));
try std.testing.expectEqual(@as(i64, 7), try Counter.get(&db, allocator, "hitsExtra"));
}
test "drop removes every field of a hash and every element+meta of a list" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
_ = try Hash.set(&db, allocator, "user:1", "name", "ada");
_ = try Hash.set(&db, allocator, "user:1", "age", "36");
try std.testing.expect(try collectionExists(&db, allocator, .hash, "user:1"));
try std.testing.expectEqual(@as(usize, 2), try dropCollection(&db, allocator, .hash, "user:1"));
try std.testing.expect(!try collectionExists(&db, allocator, .hash, "user:1"));
try std.testing.expectEqual(@as(usize, 0), try Hash.len(&db, allocator, "user:1"));
try List.pushRight(&db, allocator, "queue", "a");
try List.pushRight(&db, allocator, "queue", "b");
try std.testing.expect(try collectionExists(&db, allocator, .list, "queue"));
// 2 elements + 1 meta record.
try std.testing.expectEqual(@as(usize, 3), try dropCollection(&db, allocator, .list, "queue"));
try std.testing.expect(!try collectionExists(&db, allocator, .list, "queue"));
try std.testing.expectEqual(@as(usize, 0), try List.len(&db, allocator, "queue"));
// Dropping something never created is a harmless no-op.
try std.testing.expectEqual(@as(usize, 0), try dropCollection(&db, allocator, .set, "never-existed"));
}
test "drop removes both the member row and score-index row of every zset entry" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
try ZSet.add(&db, allocator, "leaderboard", "alice", 10);
try ZSet.add(&db, allocator, "leaderboard", "bob", 20);
try std.testing.expect(try collectionExists(&db, allocator, .zset, "leaderboard"));
// 2 members * (1 member row + 1 score-index row) = 4.
try std.testing.expectEqual(@as(usize, 4), try dropCollection(&db, allocator, .zset, "leaderboard"));
try std.testing.expect(!try collectionExists(&db, allocator, .zset, "leaderboard"));
try std.testing.expectEqual(@as(usize, 0), try ZSet.card(&db, allocator, "leaderboard"));
}
test "zset add is a no-op when the score does not change" {
const allocator = std.testing.allocator;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var db = try openTemp(&tmp, allocator);
defer db.close();
try ZSet.add(&db, allocator, "z", "a", 5.0);
try ZSet.add(&db, allocator, "z", "a", 5.0);
try std.testing.expectEqual(@as(usize, 1), try ZSet.card(&db, allocator, "z"));
try std.testing.expectEqual(@as(f64, 5.0), (try ZSet.score(&db, allocator, "z", "a")).?);
}