diff --git a/src/collections.zig b/src/collections.zig index 6d816c4..1c0157b 100644 --- a/src/collections.zig +++ b/src/collections.zig @@ -634,3 +634,160 @@ pub const List = struct { return error.TooManyConflicts; } + pub fn popLeft(db: *Db, allocator: Allocator, name: []const u8) !?[]u8 { + const mk = try metaKey(allocator, name); + defer allocator.free(mk); + + var attempt: usize = 0; + while (attempt < maxConflictRetries) : (attempt += 1) { + var txn = Transaction.begin(db); + defer txn.deinit(); + + var meta = try readMeta(&txn, allocator, mk); + if (meta.tail - meta.head <= 1) return null; + const seq = meta.head + 1; + meta.head += 1; + + const ek = try elementKey(allocator, name, seq); + defer allocator.free(ek); + const value = try txn.get(allocator, ek) orelse return error.Malformed; + errdefer allocator.free(value); + try txn.delete(ek); + try writeMeta(&txn, mk, meta); + + txn.commit() catch |err| { + allocator.free(value); + if (err == error.Conflict) continue; + return err; + }; + return value; + } + return error.TooManyConflicts; + } + + pub fn popRight(db: *Db, allocator: Allocator, name: []const u8) !?[]u8 { + const mk = try metaKey(allocator, name); + defer allocator.free(mk); + + var attempt: usize = 0; + while (attempt < maxConflictRetries) : (attempt += 1) { + var txn = Transaction.begin(db); + defer txn.deinit(); + + var meta = try readMeta(&txn, allocator, mk); + if (meta.tail - meta.head <= 1) return null; + const seq = meta.tail - 1; + meta.tail -= 1; + + const ek = try elementKey(allocator, name, seq); + defer allocator.free(ek); + const value = try txn.get(allocator, ek) orelse return error.Malformed; + errdefer allocator.free(value); + try txn.delete(ek); + try writeMeta(&txn, mk, meta); + + txn.commit() catch |err| { + allocator.free(value); + if (err == error.Conflict) continue; + return err; + }; + return value; + } + return error.TooManyConflicts; + } + + pub fn len(db: *Db, allocator: Allocator, name: []const u8) !usize { + const mk = try metaKey(allocator, name); + defer allocator.free(mk); + const raw = try db.get(allocator, mk) orelse return 0; + defer allocator.free(raw); + if (raw.len != 16) return error.Malformed; + const head = std.mem.readInt(i64, raw[0..8], .big); + const tail = std.mem.readInt(i64, raw[8..16], .big); + return @intCast(tail - head - 1); + } + + /// Redis-style inclusive range with negative indices counting from the + /// end (-1 is the last element). Out-of-range indices are clamped; + /// start > stop after clamping yields an empty result. Caller owns the + /// returned slice and each element. + pub fn range(db: *Db, allocator: Allocator, name: []const u8, start: i64, stop: i64) ![][]u8 { + const total: i64 = @intCast(try len(db, allocator, name)); + + var results = std.ArrayList([]u8).init(allocator); + errdefer { + for (results.items) |m| allocator.free(m); + results.deinit(); + } + if (total == 0) return results.toOwnedSlice(); + + var lo = if (start < 0) total + start else start; + var hi = if (stop < 0) total + stop else stop; + if (lo < 0) lo = 0; + if (hi > total - 1) hi = total - 1; + if (lo > hi) return results.toOwnedSlice(); + + const p = try elementPrefix(allocator, name); + defer allocator.free(p); + var it = try db.scanPrefix(allocator, p); + defer it.deinit(); + + var idx: i64 = 0; + while (try it.next()) |entry| { + if (idx >= lo and idx <= hi) { + try results.append(try allocator.dupe(u8, entry.value)); + } + idx += 1; + } + return results.toOwnedSlice(); + } +}; + +// --------------------------------------------------------------------- +// ZSet (sorted set: member -> score, plus a score-ordered index for +// range/rank queries) +// --------------------------------------------------------------------- + +pub const ZMember = struct { + member: []u8, + score: f64, + + pub fn free(self: ZMember, allocator: Allocator) void { + allocator.free(self.member); + } +}; + +pub const ZSet = struct { + fn memberKey(allocator: Allocator, name: []const u8, member: []const u8) ![]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, "M"); + try out.appendSlice(member); + return out.toOwnedSlice(); + } + + fn scorePrefix(allocator: Allocator, name: []const u8) ![]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"); + return out.toOwnedSlice(); + } + + fn scoreEntryKey(allocator: Allocator, name: []const u8, scoreVal: f64, member: []const u8) ![]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 }); + try row.appendOrdered(&out, .{ .text = member }); + return out.toOwnedSlice(); + } +