feat: add prefixUpperBound and Counter to collections

This commit is contained in:
dualfroz committed 2026-09-01 22:28:33 +02:00
1 parent 4c750209f1
commit ca9299e7cb
1 file changed
+163
+163
View File
@@ -159,3 +159,166 @@ pub fn dropCollection(db: *Db, allocator: Allocator, k: Kind, name: []const u8)
return 1; return 1;
} }
const p = try genericPrefix(allocator, k, name);
defer allocator.free(p);
var keys = std.ArrayList([]u8).init(allocator);
defer {
for (keys.items) |kk| allocator.free(kk);
keys.deinit();
}
{
// Collect first, delete after: db.delete mid-scan would mutate the
// memtable structure a live RangeIterator's cursors are threaded
// through (same reasoning as ttl.zig's sweepExpired).
var it = try db.scanPrefix(allocator, p);
defer it.deinit();
while (try it.next()) |entry| {
try keys.append(try allocator.dupe(u8, entry.key));
}
}
for (keys.items) |kk| try db.delete(kk);
return keys.items.len;
}
fn prefixUpperBound(allocator: Allocator, prefix: []const u8) !?[]u8 {
var end = prefix.len;
while (end > 0 and prefix[end - 1] == 0xff) end -= 1;
if (end == 0) return null;
const upper = try allocator.alloc(u8, end);
@memcpy(upper, prefix[0..end]);
upper[end - 1] += 1;
return upper;
}
// ---------------------------------------------------------------------
// Counter
// ---------------------------------------------------------------------
pub const Counter = struct {
fn key(allocator: Allocator, name: []const u8) ![]u8 {
var out = std.ArrayList(u8).init(allocator);
errdefer out.deinit();
try out.append(nsTag);
try appendSegment(&out, "C");
try out.appendSlice(name);
return out.toOwnedSlice();
}
pub fn get(db: *Db, allocator: Allocator, name: []const u8) !i64 {
const k = try key(allocator, name);
defer allocator.free(k);
const raw = try db.get(allocator, k) orelse return 0;
defer allocator.free(raw);
return readI64(raw);
}
pub fn set(db: *Db, allocator: Allocator, name: []const u8, value: i64) !void {
const k = try key(allocator, name);
defer allocator.free(k);
var out = std.ArrayList(u8).init(allocator);
defer out.deinit();
try putI64(&out, value);
try db.put(k, out.items);
}
/// Atomically adds `delta` to the counter (creating it at 0 if absent)
/// and returns the new value. Uses an OCC transaction so concurrent
/// increments never lose an update to a lost read-modify-write race.
pub fn incrBy(db: *Db, allocator: Allocator, name: []const u8, delta: i64) !i64 {
const k = try key(allocator, name);
defer allocator.free(k);
var attempt: usize = 0;
while (attempt < maxConflictRetries) : (attempt += 1) {
var txn = Transaction.begin(db);
defer txn.deinit();
const current: i64 = blk: {
const raw = try txn.get(allocator, k) orelse break :blk 0;
defer allocator.free(raw);
break :blk try readI64(raw);
};
const next = current + delta;
var out = std.ArrayList(u8).init(allocator);
defer out.deinit();
try putI64(&out, next);
try txn.put(k, out.items);
txn.commit() catch |err| {
if (err == error.Conflict) continue;
return err;
};
return next;
}
return error.TooManyConflicts;
}
};
// ---------------------------------------------------------------------
// Hash
// ---------------------------------------------------------------------
pub const HashField = struct {
field: []u8,
value: []u8,
pub fn free(self: HashField, allocator: Allocator) void {
allocator.free(self.field);
allocator.free(self.value);
}
};
pub const Hash = struct {
fn prefix(allocator: Allocator, name: []const u8) ![]u8 {
var out = std.ArrayList(u8).init(allocator);
errdefer out.deinit();
try out.append(nsTag);
try appendSegment(&out, "H");
try appendSegment(&out, name);
return out.toOwnedSlice();
}
fn fieldKey(allocator: Allocator, name: []const u8, field: []const u8) ![]u8 {
var out = std.ArrayList(u8).init(allocator);
errdefer out.deinit();
try out.append(nsTag);
try appendSegment(&out, "H");
try appendSegment(&out, name);
try out.appendSlice(field);
return out.toOwnedSlice();
}
/// Sets a field, returning true if the field was newly created.
pub fn set(db: *Db, allocator: Allocator, name: []const u8, field: []const u8, value: []const u8) !bool {
const k = try fieldKey(allocator, name, field);
defer allocator.free(k);
const existing = try db.get(allocator, k);
if (existing) |raw| allocator.free(raw);
try db.put(k, value);
return existing == null;
}
pub fn get(db: *Db, allocator: Allocator, name: []const u8, field: []const u8) !?[]u8 {
const k = try fieldKey(allocator, name, field);
defer allocator.free(k);
return db.get(allocator, k);
}
pub fn exists(db: *Db, allocator: Allocator, name: []const u8, field: []const u8) !bool {
const raw = try get(db, allocator, name, field) orelse return false;
allocator.free(raw);
return true;
}
/// Deletes a field, returning true if it existed.
pub fn del(db: *Db, allocator: Allocator, name: []const u8, field: []const u8) !bool {
const existed = try exists(db, allocator, name, field);
if (!existed) return false;
const k = try fieldKey(allocator, name, field);
defer allocator.free(k);
try db.delete(k);
return true;
}