From ca9299e7cb3bba4778f76d30dd767f4461c93f1a Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 1 Sep 2026 22:28:33 +0200 Subject: [PATCH] feat: add prefixUpperBound and Counter to collections --- src/collections.zig | 163 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/src/collections.zig b/src/collections.zig index 4f14ec3..500d83f 100644 --- a/src/collections.zig +++ b/src/collections.zig @@ -159,3 +159,166 @@ pub fn dropCollection(db: *Db, allocator: Allocator, k: Kind, name: []const u8) 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; + } +