feat: add scoreBound and add to collections
This commit is contained in:
1 parent
d8e8662d48
commit
b7a8c8a622
1 file changed
+156
@@ -791,3 +791,159 @@ pub const ZSet = struct {
|
||||
return out.toOwnedSlice();
|
||||
}
|
||||
|
||||
/// Score-index key truncated to just the ordered score (no member
|
||||
/// suffix), used as a scan bound. Every real entry is a strict byte
|
||||
/// extension of this (the member part is never empty), so a key equal to
|
||||
/// `scoreBound(s)` never itself denotes a stored member.
|
||||
fn scoreBound(allocator: Allocator, name: []const u8, scoreVal: f64) ![]u8 {
|
||||
var out = std.ArrayList(u8).init(allocator);
|
||||
errdefer out.deinit();
|
||||
try out.append(nsTag);
|
||||
try appendSegment(&out, "Z");
|
||||
try appendSegment(&out, name);
|
||||
try appendSegment(&out, "S");
|
||||
try row.appendOrdered(&out, .{ .float = scoreVal });
|
||||
return out.toOwnedSlice();
|
||||
}
|
||||
|
||||
/// Upserts `member`'s score, atomically re-pointing the score index if
|
||||
/// the member already existed with a different score.
|
||||
pub fn add(db: *Db, allocator: Allocator, name: []const u8, member: []const u8, scoreVal: f64) !void {
|
||||
const mk = try memberKey(allocator, name, member);
|
||||
defer allocator.free(mk);
|
||||
|
||||
var attempt: usize = 0;
|
||||
while (attempt < maxConflictRetries) : (attempt += 1) {
|
||||
var txn = Transaction.begin(db);
|
||||
defer txn.deinit();
|
||||
|
||||
const old = try txn.get(allocator, mk);
|
||||
if (old) |raw| {
|
||||
defer allocator.free(raw);
|
||||
const oldScore = try readF64(raw);
|
||||
if (oldScore == scoreVal) return;
|
||||
const oldEntryKey = try scoreEntryKey(allocator, name, oldScore, member);
|
||||
defer allocator.free(oldEntryKey);
|
||||
try txn.delete(oldEntryKey);
|
||||
}
|
||||
|
||||
var scoreBuf = std.ArrayList(u8).init(allocator);
|
||||
defer scoreBuf.deinit();
|
||||
try putF64(&scoreBuf, scoreVal);
|
||||
try txn.put(mk, scoreBuf.items);
|
||||
|
||||
const entryKey = try scoreEntryKey(allocator, name, scoreVal, member);
|
||||
defer allocator.free(entryKey);
|
||||
try txn.put(entryKey, &.{});
|
||||
|
||||
txn.commit() catch |err| {
|
||||
if (err == error.Conflict) continue;
|
||||
return err;
|
||||
};
|
||||
return;
|
||||
}
|
||||
return error.TooManyConflicts;
|
||||
}
|
||||
|
||||
pub fn score(db: *Db, allocator: Allocator, name: []const u8, member: []const u8) !?f64 {
|
||||
const mk = try memberKey(allocator, name, member);
|
||||
defer allocator.free(mk);
|
||||
const raw = try db.get(allocator, mk) orelse return null;
|
||||
defer allocator.free(raw);
|
||||
return try readF64(raw);
|
||||
}
|
||||
|
||||
pub fn incrBy(db: *Db, allocator: Allocator, name: []const u8, member: []const u8, delta: f64) !f64 {
|
||||
const mk = try memberKey(allocator, name, member);
|
||||
defer allocator.free(mk);
|
||||
|
||||
var attempt: usize = 0;
|
||||
while (attempt < maxConflictRetries) : (attempt += 1) {
|
||||
var txn = Transaction.begin(db);
|
||||
defer txn.deinit();
|
||||
|
||||
const oldRaw = try txn.get(allocator, mk);
|
||||
const oldScore: f64 = if (oldRaw) |raw| blk: {
|
||||
defer allocator.free(raw);
|
||||
break :blk try readF64(raw);
|
||||
} else 0.0;
|
||||
const newScore = oldScore + delta;
|
||||
|
||||
if (oldRaw != null) {
|
||||
const oldEntryKey = try scoreEntryKey(allocator, name, oldScore, member);
|
||||
defer allocator.free(oldEntryKey);
|
||||
try txn.delete(oldEntryKey);
|
||||
}
|
||||
|
||||
var scoreBuf = std.ArrayList(u8).init(allocator);
|
||||
defer scoreBuf.deinit();
|
||||
try putF64(&scoreBuf, newScore);
|
||||
try txn.put(mk, scoreBuf.items);
|
||||
|
||||
const entryKey = try scoreEntryKey(allocator, name, newScore, member);
|
||||
defer allocator.free(entryKey);
|
||||
try txn.put(entryKey, &.{});
|
||||
|
||||
txn.commit() catch |err| {
|
||||
if (err == error.Conflict) continue;
|
||||
return err;
|
||||
};
|
||||
return newScore;
|
||||
}
|
||||
return error.TooManyConflicts;
|
||||
}
|
||||
|
||||
/// Returns true if the member was present and has been removed.
|
||||
pub fn remove(db: *Db, allocator: Allocator, name: []const u8, member: []const u8) !bool {
|
||||
const mk = try memberKey(allocator, name, member);
|
||||
defer allocator.free(mk);
|
||||
|
||||
var attempt: usize = 0;
|
||||
while (attempt < maxConflictRetries) : (attempt += 1) {
|
||||
var txn = Transaction.begin(db);
|
||||
defer txn.deinit();
|
||||
|
||||
const raw = try txn.get(allocator, mk) orelse return false;
|
||||
defer allocator.free(raw);
|
||||
const existingScore = try readF64(raw);
|
||||
|
||||
try txn.delete(mk);
|
||||
const entryKey = try scoreEntryKey(allocator, name, existingScore, member);
|
||||
defer allocator.free(entryKey);
|
||||
try txn.delete(entryKey);
|
||||
|
||||
txn.commit() catch |err| {
|
||||
if (err == error.Conflict) continue;
|
||||
return err;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
return error.TooManyConflicts;
|
||||
}
|
||||
|
||||
pub fn card(db: *Db, allocator: Allocator, name: []const u8) !usize {
|
||||
const p = try scorePrefix(allocator, name);
|
||||
defer allocator.free(p);
|
||||
var it = try db.scanPrefix(allocator, p);
|
||||
defer it.deinit();
|
||||
var count: usize = 0;
|
||||
while (try it.next()) |_| count += 1;
|
||||
return count;
|
||||
}
|
||||
|
||||
fn decodeScoreEntry(allocator: Allocator, name: []const u8, key: []const u8) !ZMember {
|
||||
const p = try scorePrefix(allocator, name);
|
||||
defer allocator.free(p);
|
||||
if (key.len <= p.len or !std.mem.startsWith(u8, key, p)) return error.Malformed;
|
||||
const rest = key[p.len..];
|
||||
|
||||
// rest = [tag u8][8-byte ordered score][tag u8][escaped member][0x00 0x00]
|
||||
if (rest.len < 9 or rest[0] != @intFromEnum(row.ValueTag.float)) return error.Malformed;
|
||||
var bits = std.mem.readInt(u64, rest[1..9], .big);
|
||||
if (bits & 0x8000000000000000 != 0) {
|
||||
bits &= 0x7fffffffffffffff;
|
||||
} else {
|
||||
bits = ~bits;
|
||||
}
|
||||
const decodedScore: f64 = @bitCast(bits);
|
||||
|
||||
Reference in new issue
Block a user