Many more Redis commands (#23116)

This commit is contained in:
Alistair Smith
2025-09-30 13:27:25 -07:00
committed by GitHub
parent 5fe3e3774c
commit b613790451
7 changed files with 9510 additions and 554 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -48,10 +48,22 @@ export default [
fn: "incr",
length: 1,
},
incrby: {
fn: "incrby",
length: 2,
},
incrbyfloat: {
fn: "incrbyfloat",
length: 2,
},
decr: {
fn: "decr",
length: 1,
},
decrby: {
fn: "decrby",
length: 2,
},
exists: {
fn: "exists",
length: 1,
@@ -60,6 +72,14 @@ export default [
fn: "expire",
length: 2,
},
expireat: {
fn: "expireat",
length: 2,
},
pexpire: {
fn: "pexpire",
length: 2,
},
connect: {
fn: "jsConnect",
length: 0,
@@ -78,6 +98,14 @@ export default [
},
hmset: {
fn: "hmset",
length: 2,
},
hset: {
fn: "hset",
length: 2,
},
hsetnx: {
fn: "hsetnx",
length: 3,
},
hget: {
@@ -88,6 +116,70 @@ export default [
fn: "hmget",
length: 2,
},
hdel: {
fn: "hdel",
length: 2,
},
hexists: {
fn: "hexists",
length: 2,
},
hrandfield: {
fn: "hrandfield",
length: 1,
},
hscan: {
fn: "hscan",
length: 2,
},
hgetdel: {
fn: "hgetdel",
length: 2,
},
hgetex: {
fn: "hgetex",
length: 2,
},
hsetex: {
fn: "hsetex",
length: 3,
},
hexpire: {
fn: "hexpire",
length: 3,
},
hexpireat: {
fn: "hexpireat",
length: 3,
},
hexpiretime: {
fn: "hexpiretime",
length: 2,
},
hpersist: {
fn: "hpersist",
length: 2,
},
hpexpire: {
fn: "hpexpire",
length: 3,
},
hpexpireat: {
fn: "hpexpireat",
length: 3,
},
hpexpiretime: {
fn: "hpexpiretime",
length: 2,
},
hpttl: {
fn: "hpttl",
length: 2,
},
httl: {
fn: "httl",
length: 2,
},
sismember: {
fn: "sismember",
length: 2,
@@ -123,6 +215,42 @@ export default [
bitcount: {
fn: "bitcount",
},
blmove: {
fn: "blmove",
length: 5,
},
blmpop: {
fn: "blmpop",
length: 3,
},
blpop: {
fn: "blpop",
length: 2,
},
brpop: {
fn: "brpop",
length: 2,
},
brpoplpush: {
fn: "brpoplpush",
length: 3,
},
getbit: {
fn: "getbit",
length: 2,
},
setbit: {
fn: "setbit",
length: 3,
},
getrange: {
fn: "getrange",
length: 3,
},
setrange: {
fn: "setrange",
length: 3,
},
dump: {
fn: "dump",
},
@@ -150,33 +278,132 @@ export default [
keys: {
fn: "keys",
},
lindex: {
fn: "lindex",
length: 2,
},
linsert: {
fn: "linsert",
length: 4,
},
llen: {
fn: "llen",
},
lmove: {
fn: "lmove",
length: 4,
},
lmpop: {
fn: "lmpop",
length: 2,
},
lpop: {
fn: "lpop",
},
lpos: {
fn: "lpos",
length: 2,
},
lrange: {
fn: "lrange",
length: 3,
},
lrem: {
fn: "lrem",
length: 3,
},
lset: {
fn: "lset",
length: 3,
},
ltrim: {
fn: "ltrim",
length: 3,
},
persist: {
fn: "persist",
},
pexpireat: {
fn: "pexpireat",
length: 2,
},
pexpiretime: {
fn: "pexpiretime",
},
pttl: {
fn: "pttl",
},
randomkey: {
fn: "randomkey",
length: 0,
},
rpop: {
fn: "rpop",
},
rpoplpush: {
fn: "rpoplpush",
length: 2,
},
scan: {
fn: "scan",
},
scard: {
fn: "scard",
},
sdiff: {
fn: "sdiff",
length: 1,
},
sdiffstore: {
fn: "sdiffstore",
length: 2,
},
sinter: {
fn: "sinter",
length: 1,
},
sintercard: {
fn: "sintercard",
length: 1,
},
sinterstore: {
fn: "sinterstore",
length: 2,
},
smismember: {
fn: "smismember",
length: 2,
},
sscan: {
fn: "sscan",
length: 2,
},
strlen: {
fn: "strlen",
},
sunion: {
fn: "sunion",
length: 1,
},
sunionstore: {
fn: "sunionstore",
length: 2,
},
type: {
fn: "type",
length: 1,
},
zcard: {
fn: "zcard",
},
zcount: {
fn: "zcount",
length: 3,
},
zlexcount: {
fn: "zlexcount",
length: 3,
},
zpopmax: {
fn: "zpopmax",
},
@@ -186,6 +413,50 @@ export default [
zrandmember: {
fn: "zrandmember",
},
zrange: {
fn: "zrange",
length: 3,
},
zrangebylex: {
fn: "zrangebylex",
length: 3,
},
zrangebyscore: {
fn: "zrangebyscore",
length: 3,
},
zrangestore: {
fn: "zrangestore",
length: 4,
},
zrem: {
fn: "zrem",
length: 2,
},
zremrangebylex: {
fn: "zremrangebylex",
length: 3,
},
zremrangebyrank: {
fn: "zremrangebyrank",
length: 3,
},
zremrangebyscore: {
fn: "zremrangebyscore",
length: 3,
},
zrevrange: {
fn: "zrevrange",
length: 3,
},
zrevrangebylex: {
fn: "zrevrangebylex",
length: 3,
},
zrevrangebyscore: {
fn: "zrevrangebyscore",
length: 3,
},
append: {
fn: "append",
},
@@ -210,12 +481,85 @@ export default [
setnx: {
fn: "setnx",
},
setex: {
fn: "setex",
length: 3,
},
psetex: {
fn: "psetex",
length: 3,
},
zscore: {
fn: "zscore",
},
zincrby: {
fn: "zincrby",
length: 3,
},
zmscore: {
fn: "zmscore",
},
zadd: {
fn: "zadd",
length: 3,
},
zscan: {
fn: "zscan",
length: 2,
},
zdiff: {
fn: "zdiff",
length: 1,
},
zdiffstore: {
fn: "zdiffstore",
length: 2,
},
zinter: {
fn: "zinter",
length: 2,
},
zintercard: {
fn: "zintercard",
length: 1,
},
zinterstore: {
fn: "zinterstore",
length: 3,
},
zunion: {
fn: "zunion",
length: 2,
},
zunionstore: {
fn: "zunionstore",
length: 3,
},
zmpop: {
fn: "zmpop",
length: 2,
},
bzmpop: {
fn: "bzmpop",
length: 3,
},
bzpopmin: {
fn: "bzpopmin",
length: 2,
},
bzpopmax: {
fn: "bzpopmax",
length: 2,
},
mget: {
fn: "mget",
},
mset: {
fn: "mset",
},
msetnx: {
fn: "msetnx",
},
ping: { fn: "ping" },
publish: { fn: "publish" },
script: { fn: "script" },
@@ -232,6 +576,11 @@ export default [
unsubscribe: { fn: "unsubscribe" },
punsubscribe: { fn: "punsubscribe" },
pubsub: { fn: "pubsub" },
copy: { fn: "copy" },
unlink: { fn: "unlink" },
touch: { fn: "touch" },
rename: { fn: "rename", length: 2 },
renamenx: { fn: "renamenx", length: 2 },
},
values: ["onconnect", "onclose", "connectionPromise", "hello", "subscriptionCallbackMap"],
}),

View File

@@ -1243,72 +1243,163 @@ pub const JSValkeyClient = struct {
pub const @"type" = fns.type;
pub const append = fns.append;
pub const bitcount = fns.bitcount;
pub const blpop = fns.blpop;
pub const brpop = fns.brpop;
pub const copy = fns.copy;
pub const decr = fns.decr;
pub const decrby = fns.decrby;
pub const del = fns.del;
pub const dump = fns.dump;
pub const duplicate = fns.duplicate;
pub const exists = fns.exists;
pub const expire = fns.expire;
pub const expireat = fns.expireat;
pub const expiretime = fns.expiretime;
pub const get = fns.get;
pub const getBuffer = fns.getBuffer;
pub const getbit = fns.getbit;
pub const getdel = fns.getdel;
pub const getex = fns.getex;
pub const getrange = fns.getrange;
pub const getset = fns.getset;
pub const hgetall = fns.hgetall;
pub const hget = fns.hget;
pub const hincrby = fns.hincrby;
pub const hincrbyfloat = fns.hincrbyfloat;
pub const hkeys = fns.hkeys;
pub const hdel = fns.hdel;
pub const hexists = fns.hexists;
pub const hgetdel = fns.hgetdel;
pub const hgetex = fns.hgetex;
pub const hlen = fns.hlen;
pub const hmget = fns.hmget;
pub const hmset = fns.hmset;
pub const hrandfield = fns.hrandfield;
pub const hscan = fns.hscan;
pub const hset = fns.hset;
pub const hsetex = fns.hsetex;
pub const hsetnx = fns.hsetnx;
pub const hstrlen = fns.hstrlen;
pub const hvals = fns.hvals;
pub const hexpire = fns.hexpire;
pub const hexpireat = fns.hexpireat;
pub const hexpiretime = fns.hexpiretime;
pub const hpersist = fns.hpersist;
pub const hpexpire = fns.hpexpire;
pub const hpexpireat = fns.hpexpireat;
pub const hpexpiretime = fns.hpexpiretime;
pub const hpttl = fns.hpttl;
pub const httl = fns.httl;
pub const incr = fns.incr;
pub const incrby = fns.incrby;
pub const incrbyfloat = fns.incrbyfloat;
pub const keys = fns.keys;
pub const lindex = fns.lindex;
pub const linsert = fns.linsert;
pub const llen = fns.llen;
pub const lmove = fns.lmove;
pub const lmpop = fns.lmpop;
pub const lpop = fns.lpop;
pub const lpos = fns.lpos;
pub const lpush = fns.lpush;
pub const lpushx = fns.lpushx;
pub const lrange = fns.lrange;
pub const lrem = fns.lrem;
pub const lset = fns.lset;
pub const ltrim = fns.ltrim;
pub const mget = fns.mget;
pub const mset = fns.mset;
pub const msetnx = fns.msetnx;
pub const persist = fns.persist;
pub const pexpire = fns.pexpire;
pub const pexpireat = fns.pexpireat;
pub const pexpiretime = fns.pexpiretime;
pub const pfadd = fns.pfadd;
pub const ping = fns.ping;
pub const psetex = fns.psetex;
pub const psubscribe = fns.psubscribe;
pub const pttl = fns.pttl;
pub const publish = fns.publish;
pub const pubsub = fns.pubsub;
pub const punsubscribe = fns.punsubscribe;
pub const randomkey = fns.randomkey;
pub const rename = fns.rename;
pub const renamenx = fns.renamenx;
pub const rpop = fns.rpop;
pub const rpoplpush = fns.rpoplpush;
pub const rpush = fns.rpush;
pub const rpushx = fns.rpushx;
pub const sadd = fns.sadd;
pub const scan = fns.scan;
pub const scard = fns.scard;
pub const script = fns.script;
pub const sdiff = fns.sdiff;
pub const sdiffstore = fns.sdiffstore;
pub const sinter = fns.sinter;
pub const sintercard = fns.sintercard;
pub const sinterstore = fns.sinterstore;
pub const select = fns.select;
pub const set = fns.set;
pub const setbit = fns.setbit;
pub const setex = fns.setex;
pub const setnx = fns.setnx;
pub const setrange = fns.setrange;
pub const sismember = fns.sismember;
pub const smembers = fns.smembers;
pub const smismember = fns.smismember;
pub const smove = fns.smove;
pub const spop = fns.spop;
pub const spublish = fns.spublish;
pub const srandmember = fns.srandmember;
pub const srem = fns.srem;
pub const sscan = fns.sscan;
pub const strlen = fns.strlen;
pub const subscribe = fns.subscribe;
pub const substr = fns.substr;
pub const sunion = fns.sunion;
pub const sunionstore = fns.sunionstore;
pub const touch = fns.touch;
pub const ttl = fns.ttl;
pub const unlink = fns.unlink;
pub const unsubscribe = fns.unsubscribe;
pub const zcard = fns.zcard;
pub const zcount = fns.zcount;
pub const zlexcount = fns.zlexcount;
pub const zpopmax = fns.zpopmax;
pub const zpopmin = fns.zpopmin;
pub const zrandmember = fns.zrandmember;
pub const zrange = fns.zrange;
pub const zrangebylex = fns.zrangebylex;
pub const zrangebyscore = fns.zrangebyscore;
pub const zrangestore = fns.zrangestore;
pub const zrank = fns.zrank;
pub const zrem = fns.zrem;
pub const zremrangebylex = fns.zremrangebylex;
pub const zremrangebyrank = fns.zremrangebyrank;
pub const zremrangebyscore = fns.zremrangebyscore;
pub const zrevrange = fns.zrevrange;
pub const zrevrangebylex = fns.zrevrangebylex;
pub const zrevrangebyscore = fns.zrevrangebyscore;
pub const zrevrank = fns.zrevrank;
pub const zscore = fns.zscore;
pub const zincrby = fns.zincrby;
pub const zmscore = fns.zmscore;
pub const zadd = fns.zadd;
pub const zscan = fns.zscan;
pub const zdiff = fns.zdiff;
pub const zdiffstore = fns.zdiffstore;
pub const zinter = fns.zinter;
pub const zintercard = fns.zintercard;
pub const zinterstore = fns.zinterstore;
pub const zunion = fns.zunion;
pub const zunionstore = fns.zunionstore;
pub const zmpop = fns.zmpop;
pub const bzmpop = fns.bzmpop;
pub const bzpopmin = fns.bzpopmin;
pub const bzpopmax = fns.bzpopmax;
pub const blmove = fns.blmove;
pub const blmpop = fns.blmpop;
pub const brpoplpush = fns.brpoplpush;
const fns = @import("./js_valkey_functions.zig");
};

View File

@@ -274,14 +274,34 @@ pub fn ttl(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe:
pub fn srem(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
try requireNotSubscriber(this, @src().fn_name);
const args_view = callframe.arguments();
if (args_view.len < 2) {
return globalObject.throw("SREM requires at least a key and one member", .{});
}
var stack_fallback = std.heap.stackFallback(512, bun.default_allocator);
var args = try std.ArrayList(JSArgument).initCapacity(stack_fallback.get(), args_view.len);
defer {
for (args.items) |*item| {
item.deinit();
}
args.deinit();
}
const key = (try fromJS(globalObject, callframe.argument(0))) orelse {
return globalObject.throwInvalidArgumentType("srem", "key", "string or buffer");
};
defer key.deinit();
const value = (try fromJS(globalObject, callframe.argument(1))) orelse {
return globalObject.throwInvalidArgumentType("srem", "value", "string or buffer");
};
defer value.deinit();
args.appendAssumeCapacity(key);
for (args_view[1..]) |arg| {
if (arg.isUndefinedOrNull()) {
break;
}
const value = (try fromJS(globalObject, arg)) orelse {
return globalObject.throwInvalidArgumentType("srem", "member", "string or buffer");
};
args.appendAssumeCapacity(value);
}
// Send SREM command
const promise = this.send(
@@ -289,7 +309,7 @@ pub fn srem(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe:
callframe.this(),
&.{
.command = "SREM",
.args = .{ .args = &.{ key, value } },
.args = .{ .args = args.items },
},
) catch |err| {
return protocol.valkeyErrorToJS(globalObject, "Failed to send SREM command", err);
@@ -301,10 +321,28 @@ pub fn srem(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe:
pub fn srandmember(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
try requireNotSubscriber(this, @src().fn_name);
const args_view = callframe.arguments();
var stack_fallback = std.heap.stackFallback(512, bun.default_allocator);
var args = try std.ArrayList(JSArgument).initCapacity(stack_fallback.get(), args_view.len);
defer {
for (args.items) |*item| {
item.deinit();
}
args.deinit();
}
const key = (try fromJS(globalObject, callframe.argument(0))) orelse {
return globalObject.throwInvalidArgumentType("srandmember", "key", "string or buffer");
};
defer key.deinit();
args.appendAssumeCapacity(key);
// Optional count argument
if (args_view.len > 1 and !callframe.argument(1).isUndefinedOrNull()) {
const count_arg = try fromJS(globalObject, callframe.argument(1)) orelse {
return globalObject.throwInvalidArgumentType("srandmember", "count", "number or string");
};
args.appendAssumeCapacity(count_arg);
}
// Send SRANDMEMBER command
const promise = this.send(
@@ -312,7 +350,7 @@ pub fn srandmember(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, cal
callframe.this(),
&.{
.command = "SRANDMEMBER",
.args = .{ .args = &.{key} },
.args = .{ .args = args.items },
},
) catch |err| {
return protocol.valkeyErrorToJS(globalObject, "Failed to send SRANDMEMBER command", err);
@@ -347,10 +385,28 @@ pub fn smembers(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callfr
pub fn spop(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
try requireNotSubscriber(this, @src().fn_name);
const args_view = callframe.arguments();
var stack_fallback = std.heap.stackFallback(512, bun.default_allocator);
var args = try std.ArrayList(JSArgument).initCapacity(stack_fallback.get(), args_view.len);
defer {
for (args.items) |*item| {
item.deinit();
}
args.deinit();
}
const key = (try fromJS(globalObject, callframe.argument(0))) orelse {
return globalObject.throwInvalidArgumentType("spop", "key", "string or buffer");
};
defer key.deinit();
args.appendAssumeCapacity(key);
// Optional count argument
if (args_view.len > 1 and !callframe.argument(1).isUndefinedOrNull()) {
const count_arg = try fromJS(globalObject, callframe.argument(1)) orelse {
return globalObject.throwInvalidArgumentType("spop", "count", "number or string");
};
args.appendAssumeCapacity(count_arg);
}
// Send SPOP command
const promise = this.send(
@@ -358,7 +414,7 @@ pub fn spop(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe:
callframe.this(),
&.{
.command = "SPOP",
.args = .{ .args = &.{key} },
.args = .{ .args = args.items },
},
) catch |err| {
return protocol.valkeyErrorToJS(globalObject, "Failed to send SPOP command", err);
@@ -370,14 +426,34 @@ pub fn spop(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe:
pub fn sadd(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
try requireNotSubscriber(this, @src().fn_name);
const args_view = callframe.arguments();
if (args_view.len < 2) {
return globalObject.throw("SADD requires at least a key and one member", .{});
}
var stack_fallback = std.heap.stackFallback(512, bun.default_allocator);
var args = try std.ArrayList(JSArgument).initCapacity(stack_fallback.get(), args_view.len);
defer {
for (args.items) |*item| {
item.deinit();
}
args.deinit();
}
const key = (try fromJS(globalObject, callframe.argument(0))) orelse {
return globalObject.throwInvalidArgumentType("sadd", "key", "string or buffer");
};
defer key.deinit();
const value = (try fromJS(globalObject, callframe.argument(1))) orelse {
return globalObject.throwInvalidArgumentType("sadd", "value", "string or buffer");
};
defer value.deinit();
args.appendAssumeCapacity(key);
for (args_view[1..]) |arg| {
if (arg.isUndefinedOrNull()) {
break;
}
const value = (try fromJS(globalObject, arg)) orelse {
return globalObject.throwInvalidArgumentType("sadd", "member", "string or buffer");
};
args.appendAssumeCapacity(value);
}
// Send SADD command
const promise = this.send(
@@ -385,7 +461,7 @@ pub fn sadd(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe:
callframe.this(),
&.{
.command = "SADD",
.args = .{ .args = &.{ key, value } },
.args = .{ .args = args.items },
},
) catch |err| {
return protocol.valkeyErrorToJS(globalObject, "Failed to send SADD command", err);
@@ -425,35 +501,49 @@ pub fn sismember(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callf
pub fn hmget(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
try requireNotSubscriber(this, @src().fn_name);
const key = (try fromJS(globalObject, callframe.argument(0))) orelse {
return globalObject.throwInvalidArgumentType("hmget", "key", "string or buffer");
};
defer key.deinit();
// Get field array argument
const fields_array = callframe.argument(1);
if (!fields_array.isObject() or !fields_array.isArray()) {
return globalObject.throw("Fields must be an array", .{});
const args_view = callframe.arguments();
if (args_view.len < 2) {
return globalObject.throw("HMGET requires at least a key and one field", .{});
}
var iter = try fields_array.arrayIterator(globalObject);
var args = try std.ArrayList(jsc.ZigString.Slice).initCapacity(bun.default_allocator, iter.len + 1);
var stack_fallback = std.heap.stackFallback(512, bun.default_allocator);
var args = try std.ArrayList(JSArgument).initCapacity(stack_fallback.get(), args_view.len);
defer {
for (args.items) |item| {
for (args.items) |*item| {
item.deinit();
}
args.deinit();
}
args.appendAssumeCapacity(jsc.ZigString.Slice.fromUTF8NeverFree(key.slice()));
const key = (try fromJS(globalObject, callframe.argument(0))) orelse {
return globalObject.throwInvalidArgumentType("hmget", "key", "string or buffer");
};
args.appendAssumeCapacity(key);
// Add field names as arguments
while (try iter.next()) |field_js| {
const field_str = try field_js.toBunString(globalObject);
defer field_str.deref();
const second_arg = callframe.argument(1);
if (second_arg.isArray()) {
const array_len = try second_arg.getLength(globalObject);
if (array_len == 0) {
return globalObject.throw("HMGET requires at least one field", .{});
}
const field_slice = field_str.toUTF8WithoutRef(bun.default_allocator);
args.appendAssumeCapacity(field_slice);
var array_iter = try second_arg.arrayIterator(globalObject);
while (try array_iter.next()) |element| {
const field = (try fromJS(globalObject, element)) orelse {
return globalObject.throwInvalidArgumentType("hmget", "field", "string or buffer");
};
try args.append(field);
}
} else {
for (args_view[1..]) |arg| {
if (arg.isUndefinedOrNull()) {
break;
}
const field = (try fromJS(globalObject, arg)) orelse {
return globalObject.throwInvalidArgumentType("hmget", "field", "string or buffer");
};
try args.append(field);
}
}
// Send HMGET command
@@ -462,7 +552,7 @@ pub fn hmget(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe
callframe.this(),
&.{
.command = "HMGET",
.args = .{ .slices = args.items },
.args = .{ .args = args.items },
},
) catch |err| {
return protocol.valkeyErrorToJS(globalObject, "Failed to send HMGET command", err);
@@ -534,66 +624,183 @@ pub fn hincrbyfloat(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, ca
return promise.toJS();
}
// Implement hmset (set multiple values in hash)
pub fn hmset(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
try requireNotSubscriber(this, @src().fn_name);
fn hsetImpl(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, comptime command: []const u8) bun.JSError!JSValue {
try requireNotSubscriber(this, command);
const key = try callframe.argument(0).toBunString(globalObject);
defer key.deref();
// For simplicity, let's accept a list of alternating keys and values
const array_arg = callframe.argument(1);
if (!array_arg.isObject() or !array_arg.isArray()) {
return globalObject.throw("Arguments must be an array of alternating field names and values", .{});
}
const second_arg = callframe.argument(1);
var iter = try array_arg.arrayIterator(globalObject);
if (iter.len % 2 != 0) {
return globalObject.throw("Arguments must be an array of alternating field names and values", .{});
}
var args = try std.ArrayList(jsc.ZigString.Slice).initCapacity(bun.default_allocator, iter.len + 1);
var args = std.ArrayList(jsc.ZigString.Slice).init(bun.default_allocator);
defer {
for (args.items) |item| {
item.deinit();
}
for (args.items) |item| item.deinit();
args.deinit();
}
// Add key as first argument
const key_slice = key.toUTF8WithoutRef(bun.default_allocator);
defer key_slice.deinit();
args.appendAssumeCapacity(key_slice);
try args.append(key.toUTF8(bun.default_allocator));
// Add field-value pairs
while (try iter.next()) |field_js| {
// Add field name
const field_str = try field_js.toBunString(globalObject);
defer field_str.deref();
const field_slice = field_str.toUTF8WithoutRef(bun.default_allocator);
args.appendAssumeCapacity(field_slice);
if (second_arg.isObject() and !second_arg.isArray()) {
// Pattern 1: Object/Record - hset(key, {field: value, ...})
const obj = second_arg.getObject() orelse {
return globalObject.throwInvalidArgumentType(command, "fields", "object");
};
// Add value
if (try iter.next()) |value_js| {
const value_str = try value_js.toBunString(globalObject);
var object_iter = try jsc.JSPropertyIterator(.{
.skip_empty_name = false,
.include_value = true,
}).init(globalObject, obj);
defer object_iter.deinit();
try args.ensureTotalCapacity(1 + object_iter.len * 2);
while (try object_iter.next()) |field_name| {
const field_slice = field_name.toUTF8(bun.default_allocator);
args.appendAssumeCapacity(field_slice);
const value_str = try object_iter.value.toBunString(globalObject);
defer value_str.deref();
const value_slice = value_str.toUTF8WithoutRef(bun.default_allocator);
const value_slice = value_str.toUTF8(bun.default_allocator);
args.appendAssumeCapacity(value_slice);
} else {
return globalObject.throw("Arguments must be an array of alternating field names and values", .{});
}
} else if (second_arg.isArray()) {
// Pattern 3: Array - hmset(key, [field, value, ...])
var iter = try second_arg.arrayIterator(globalObject);
if (iter.len % 2 != 0) {
return globalObject.throw("Array must have an even number of elements (field-value pairs)", .{});
}
try args.ensureTotalCapacity(1 + iter.len);
while (try iter.next()) |field_js| {
const field_str = try field_js.toBunString(globalObject);
args.appendAssumeCapacity(field_str.toUTF8(bun.default_allocator));
field_str.deref();
const value_js = try iter.next() orelse {
return globalObject.throw("Array must have an even number of elements (field-value pairs)", .{});
};
const value_str = try value_js.toBunString(globalObject);
args.appendAssumeCapacity(value_str.toUTF8(bun.default_allocator));
value_str.deref();
}
} else {
// Pattern 2: Variadic - hset(key, field, value, ...)
const args_count = callframe.argumentsCount();
if (args_count < 3) {
return globalObject.throw("HSET requires at least key, field, and value arguments", .{});
}
const field_value_count = args_count - 1; // Exclude key
if (field_value_count % 2 != 0) {
return globalObject.throw("HSET requires field-value pairs (even number of arguments after key)", .{});
}
try args.ensureTotalCapacity(args_count);
var i: u32 = 1;
while (i < args_count) : (i += 1) {
const arg_str = try callframe.argument(i).toBunString(globalObject);
args.appendAssumeCapacity(arg_str.toUTF8(bun.default_allocator));
arg_str.deref();
}
}
// Send HMSET command
if (args.items.len == 1) {
return globalObject.throw("HSET requires at least one field-value pair", .{});
}
const promise = this.send(
globalObject,
callframe.this(),
&.{
.command = "HMSET",
.command = command,
.args = .{ .slices = args.items },
},
) catch |err| {
return protocol.valkeyErrorToJS(globalObject, "Failed to send HMSET command", err);
const msg = if (bun.strings.eqlComptime(command, "HSET")) "Failed to send HSET command" else "Failed to send HMSET command";
return protocol.valkeyErrorToJS(globalObject, msg, err);
};
return promise.toJS();
}
pub fn hset(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
return hsetImpl(this, globalObject, callframe, "HSET");
}
pub fn hmset(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
return hsetImpl(this, globalObject, callframe, "HMSET");
}
pub const hdel = compile.@"(key: RedisKey, ...args: RedisKey[])"("hdel", "HDEL", "key", .not_subscriber).call;
pub const hrandfield = compile.@"(key: RedisKey, ...args: RedisKey[])"("hrandfield", "HRANDFIELD", "key", .not_subscriber).call;
pub const hscan = compile.@"(key: RedisKey, ...args: RedisKey[])"("hscan", "HSCAN", "key", .not_subscriber).call;
pub const hgetdel = compile.@"(...strings: string[])"("hgetdel", "HGETDEL", .not_subscriber).call;
pub const hgetex = compile.@"(...strings: string[])"("hgetex", "HGETEX", .not_subscriber).call;
pub const hsetex = compile.@"(...strings: string[])"("hsetex", "HSETEX", .not_subscriber).call;
pub const hexpire = compile.@"(...strings: string[])"("hexpire", "HEXPIRE", .not_subscriber).call;
pub const hexpireat = compile.@"(...strings: string[])"("hexpireat", "HEXPIREAT", .not_subscriber).call;
pub const hexpiretime = compile.@"(...strings: string[])"("hexpiretime", "HEXPIRETIME", .not_subscriber).call;
pub const hpersist = compile.@"(...strings: string[])"("hpersist", "HPERSIST", .not_subscriber).call;
pub const hpexpire = compile.@"(...strings: string[])"("hpexpire", "HPEXPIRE", .not_subscriber).call;
pub const hpexpireat = compile.@"(...strings: string[])"("hpexpireat", "HPEXPIREAT", .not_subscriber).call;
pub const hpexpiretime = compile.@"(...strings: string[])"("hpexpiretime", "HPEXPIRETIME", .not_subscriber).call;
pub const hpttl = compile.@"(...strings: string[])"("hpttl", "HPTTL", .not_subscriber).call;
pub const httl = compile.@"(...strings: string[])"("httl", "HTTL", .not_subscriber).call;
pub fn hsetnx(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
try requireNotSubscriber(this, "hsetnx");
const key = (try fromJS(globalObject, callframe.argument(0))) orelse {
return globalObject.throwInvalidArgumentType("hsetnx", "key", "string or buffer");
};
defer key.deinit();
const field = (try fromJS(globalObject, callframe.argument(1))) orelse {
return globalObject.throwInvalidArgumentType("hsetnx", "field", "string or buffer");
};
defer field.deinit();
const value = (try fromJS(globalObject, callframe.argument(2))) orelse {
return globalObject.throwInvalidArgumentType("hsetnx", "value", "string or buffer");
};
defer value.deinit();
const promise = this.send(
globalObject,
callframe.this(),
&.{
.command = "HSETNX",
.args = .{ .args = &.{ key, field, value } },
.meta = .{ .return_as_bool = true },
},
) catch |err| {
return protocol.valkeyErrorToJS(globalObject, "Failed to send HSETNX command", err);
};
return promise.toJS();
}
pub fn hexists(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
try requireNotSubscriber(this, "hexists");
const key = (try fromJS(globalObject, callframe.argument(0))) orelse
return globalObject.throwInvalidArgumentType("hexists", "key", "string or buffer");
defer key.deinit();
const field = (try fromJS(globalObject, callframe.argument(1))) orelse
return globalObject.throwInvalidArgumentType("hexists", "field", "string or buffer");
defer field.deinit();
const promise = this.send(
globalObject,
callframe.this(),
&.{
.command = "HEXISTS",
.args = .{ .args = &.{ key, field } },
.meta = .{ .return_as_bool = true },
},
) catch |err| {
return protocol.valkeyErrorToJS(globalObject, "Failed to send HEXISTS command", err);
};
return promise.toJS();
}
@@ -631,7 +838,17 @@ pub fn ping(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe:
}
pub const bitcount = compile.@"(key: RedisKey)"("bitcount", "BITCOUNT", "key", .not_subscriber).call;
pub const blmove = compile.@"(...strings: string[])"("blmove", "BLMOVE", .not_subscriber).call;
pub const blmpop = compile.@"(...strings: string[])"("blmpop", "BLMPOP", .not_subscriber).call;
pub const blpop = compile.@"(...strings: string[])"("blpop", "BLPOP", .not_subscriber).call;
pub const brpop = compile.@"(...strings: string[])"("brpop", "BRPOP", .not_subscriber).call;
pub const brpoplpush = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("brpoplpush", "BRPOPLPUSH", "source", "destination", "timeout", .not_subscriber).call;
pub const getbit = compile.@"(key: RedisKey, value: RedisValue)"("getbit", "GETBIT", "key", "offset", .not_subscriber).call;
pub const setbit = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("setbit", "SETBIT", "key", "offset", "value", .not_subscriber).call;
pub const getrange = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("getrange", "GETRANGE", "key", "start", "end", .not_subscriber).call;
pub const setrange = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("setrange", "SETRANGE", "key", "offset", "value", .not_subscriber).call;
pub const dump = compile.@"(key: RedisKey)"("dump", "DUMP", "key", .not_subscriber).call;
pub const expireat = compile.@"(key: RedisKey, value: RedisValue)"("expireat", "EXPIREAT", "key", "timestamp", .not_subscriber).call;
pub const expiretime = compile.@"(key: RedisKey)"("expiretime", "EXPIRETIME", "key", .not_subscriber).call;
pub const getdel = compile.@"(key: RedisKey)"("getdel", "GETDEL", "key", .not_subscriber).call;
pub const getex = compile.@"(...strings: string[])"("getex", "GETEX", .not_subscriber).call;
@@ -640,45 +857,133 @@ pub const hkeys = compile.@"(key: RedisKey)"("hkeys", "HKEYS", "key", .not_subsc
pub const hlen = compile.@"(key: RedisKey)"("hlen", "HLEN", "key", .not_subscriber).call;
pub const hvals = compile.@"(key: RedisKey)"("hvals", "HVALS", "key", .not_subscriber).call;
pub const keys = compile.@"(key: RedisKey)"("keys", "KEYS", "key", .not_subscriber).call;
pub const lindex = compile.@"(key: RedisKey, value: RedisValue)"("lindex", "LINDEX", "key", "index", .not_subscriber).call;
pub const linsert = compile.@"(...strings: string[])"("linsert", "LINSERT", .not_subscriber).call;
pub const llen = compile.@"(key: RedisKey)"("llen", "LLEN", "key", .not_subscriber).call;
pub const lpop = compile.@"(key: RedisKey)"("lpop", "LPOP", "key", .not_subscriber).call;
pub const lmove = compile.@"(...strings: string[])"("lmove", "LMOVE", .not_subscriber).call;
pub const lmpop = compile.@"(...strings: string[])"("lmpop", "LMPOP", .not_subscriber).call;
pub const lpop = compile.@"(key: RedisKey, ...args: RedisKey[])"("lpop", "LPOP", "key", .not_subscriber).call;
pub const lpos = compile.@"(...strings: string[])"("lpos", "LPOS", .not_subscriber).call;
pub const lrange = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("lrange", "LRANGE", "key", "start", "stop", .not_subscriber).call;
pub const lrem = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("lrem", "LREM", "key", "count", "element", .not_subscriber).call;
pub const lset = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("lset", "LSET", "key", "index", "element", .not_subscriber).call;
pub const ltrim = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("ltrim", "LTRIM", "key", "start", "stop", .not_subscriber).call;
pub const persist = compile.@"(key: RedisKey)"("persist", "PERSIST", "key", .not_subscriber).call;
pub const pexpire = compile.@"(key: RedisKey, value: RedisValue)"("pexpire", "PEXPIRE", "key", "milliseconds", .not_subscriber).call;
pub const pexpireat = compile.@"(key: RedisKey, value: RedisValue)"("pexpireat", "PEXPIREAT", "key", "milliseconds-timestamp", .not_subscriber).call;
pub const pexpiretime = compile.@"(key: RedisKey)"("pexpiretime", "PEXPIRETIME", "key", .not_subscriber).call;
pub const pttl = compile.@"(key: RedisKey)"("pttl", "PTTL", "key", .not_subscriber).call;
pub const rpop = compile.@"(key: RedisKey)"("rpop", "RPOP", "key", .not_subscriber).call;
pub const randomkey = compile.@"()"("randomkey", "RANDOMKEY", .not_subscriber).call;
pub const rpop = compile.@"(key: RedisKey, ...args: RedisKey[])"("rpop", "RPOP", "key", .not_subscriber).call;
pub const rpoplpush = compile.@"(key: RedisKey, value: RedisValue)"("rpoplpush", "RPOPLPUSH", "source", "destination", .not_subscriber).call;
pub const scan = compile.@"(...strings: string[])"("scan", "SCAN", .not_subscriber).call;
pub const scard = compile.@"(key: RedisKey)"("scard", "SCARD", "key", .not_subscriber).call;
pub const sdiff = compile.@"(...strings: string[])"("sdiff", "SDIFF", .not_subscriber).call;
pub const sdiffstore = compile.@"(...strings: string[])"("sdiffstore", "SDIFFSTORE", .not_subscriber).call;
pub const sinter = compile.@"(...strings: string[])"("sinter", "SINTER", .not_subscriber).call;
pub const sintercard = compile.@"(...strings: string[])"("sintercard", "SINTERCARD", .not_subscriber).call;
pub const sinterstore = compile.@"(...strings: string[])"("sinterstore", "SINTERSTORE", .not_subscriber).call;
pub const smismember = compile.@"(...strings: string[])"("smismember", "SMISMEMBER", .not_subscriber).call;
pub const sscan = compile.@"(...strings: string[])"("sscan", "SSCAN", .not_subscriber).call;
pub const strlen = compile.@"(key: RedisKey)"("strlen", "STRLEN", "key", .not_subscriber).call;
pub const sunion = compile.@"(...strings: string[])"("sunion", "SUNION", .not_subscriber).call;
pub const sunionstore = compile.@"(...strings: string[])"("sunionstore", "SUNIONSTORE", .not_subscriber).call;
pub const @"type" = compile.@"(key: RedisKey)"("type", "TYPE", "key", .not_subscriber).call;
pub const zcard = compile.@"(key: RedisKey)"("zcard", "ZCARD", "key", .not_subscriber).call;
pub const zpopmax = compile.@"(key: RedisKey)"("zpopmax", "ZPOPMAX", "key", .not_subscriber).call;
pub const zpopmin = compile.@"(key: RedisKey)"("zpopmin", "ZPOPMIN", "key", .not_subscriber).call;
pub const zrandmember = compile.@"(key: RedisKey)"("zrandmember", "ZRANDMEMBER", "key", .not_subscriber).call;
pub const zcount = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zcount", "ZCOUNT", "key", "min", "max", .not_subscriber).call;
pub const zlexcount = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zlexcount", "ZLEXCOUNT", "key", "min", "max", .not_subscriber).call;
pub const zpopmax = compile.@"(key: RedisKey, ...args: RedisKey[])"("zpopmax", "ZPOPMAX", "key", .not_subscriber).call;
pub const zpopmin = compile.@"(key: RedisKey, ...args: RedisKey[])"("zpopmin", "ZPOPMIN", "key", .not_subscriber).call;
pub const zrandmember = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrandmember", "ZRANDMEMBER", "key", .not_subscriber).call;
pub const zrange = compile.@"(...strings: string[])"("zrange", "ZRANGE", .not_subscriber).call;
pub const zrevrange = compile.@"(...strings: string[])"("zrevrange", "ZREVRANGE", .not_subscriber).call;
pub const zrangebyscore = compile.@"(...strings: string[])"("zrangebyscore", "ZRANGEBYSCORE", .not_subscriber).call;
pub const zrevrangebyscore = compile.@"(...strings: string[])"("zrevrangebyscore", "ZREVRANGEBYSCORE", .not_subscriber).call;
pub const zrangebylex = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrangebylex", "ZRANGEBYLEX", "key", .not_subscriber).call;
pub const zrevrangebylex = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrevrangebylex", "ZREVRANGEBYLEX", "key", .not_subscriber).call;
pub const append = compile.@"(key: RedisKey, value: RedisValue)"("append", "APPEND", "key", "value", .not_subscriber).call;
pub const getset = compile.@"(key: RedisKey, value: RedisValue)"("getset", "GETSET", "key", "value", .not_subscriber).call;
pub const hget = compile.@"(key: RedisKey, value: RedisValue)"("hget", "HGET", "key", "field", .not_subscriber).call;
pub const incrby = compile.@"(key: RedisKey, value: RedisValue)"("incrby", "INCRBY", "key", "increment", .not_subscriber).call;
pub const incrbyfloat = compile.@"(key: RedisKey, value: RedisValue)"("incrbyfloat", "INCRBYFLOAT", "key", "increment", .not_subscriber).call;
pub const decrby = compile.@"(key: RedisKey, value: RedisValue)"("decrby", "DECRBY", "key", "decrement", .not_subscriber).call;
pub const lpush = compile.@"(key: RedisKey, value: RedisValue, ...args: RedisValue)"("lpush", "LPUSH", .not_subscriber).call;
pub const lpushx = compile.@"(key: RedisKey, value: RedisValue, ...args: RedisValue)"("lpushx", "LPUSHX", .not_subscriber).call;
pub const pfadd = compile.@"(key: RedisKey, value: RedisValue)"("pfadd", "PFADD", "key", "value", .not_subscriber).call;
pub const rpush = compile.@"(key: RedisKey, value: RedisValue, ...args: RedisValue)"("rpush", "RPUSH", .not_subscriber).call;
pub const rpushx = compile.@"(key: RedisKey, value: RedisValue, ...args: RedisValue)"("rpushx", "RPUSHX", .not_subscriber).call;
pub const setnx = compile.@"(key: RedisKey, value: RedisValue)"("setnx", "SETNX", "key", "value", .not_subscriber).call;
pub const setex = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("setex", "SETEX", "key", "seconds", "value", .not_subscriber).call;
pub const psetex = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("psetex", "PSETEX", "key", "milliseconds", "value", .not_subscriber).call;
pub const zscore = compile.@"(key: RedisKey, value: RedisValue)"("zscore", "ZSCORE", "key", "value", .not_subscriber).call;
pub const zincrby = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zincrby", "ZINCRBY", "key", "increment", "member", .not_subscriber).call;
pub const zmscore = compile.@"(key: RedisKey, value: RedisValue, ...args: RedisValue)"("zmscore", "ZMSCORE", .not_subscriber).call;
pub const zadd = compile.@"(...strings: string[])"("zadd", "ZADD", .not_subscriber).call;
pub const zscan = compile.@"(...strings: string[])"("zscan", "ZSCAN", .not_subscriber).call;
pub const zdiff = compile.@"(...strings: string[])"("zdiff", "ZDIFF", .not_subscriber).call;
pub const zdiffstore = compile.@"(...strings: string[])"("zdiffstore", "ZDIFFSTORE", .not_subscriber).call;
pub const zinter = compile.@"(...strings: string[])"("zinter", "ZINTER", .not_subscriber).call;
pub const zintercard = compile.@"(...strings: string[])"("zintercard", "ZINTERCARD", .not_subscriber).call;
pub const zinterstore = compile.@"(...strings: string[])"("zinterstore", "ZINTERSTORE", .not_subscriber).call;
pub const zunion = compile.@"(...strings: string[])"("zunion", "ZUNION", .not_subscriber).call;
pub const zunionstore = compile.@"(...strings: string[])"("zunionstore", "ZUNIONSTORE", .not_subscriber).call;
pub const zmpop = compile.@"(...strings: string[])"("zmpop", "ZMPOP", .not_subscriber).call;
pub const bzmpop = compile.@"(...strings: string[])"("bzmpop", "BZMPOP", .not_subscriber).call;
pub const bzpopmin = compile.@"(...strings: string[])"("bzpopmin", "BZPOPMIN", .not_subscriber).call;
pub const bzpopmax = compile.@"(...strings: string[])"("bzpopmax", "BZPOPMAX", .not_subscriber).call;
pub const del = compile.@"(key: RedisKey, ...args: RedisKey[])"("del", "DEL", "key", .not_subscriber).call;
pub const mget = compile.@"(key: RedisKey, ...args: RedisKey[])"("mget", "MGET", "key", .not_subscriber).call;
pub const mset = compile.@"(...strings: string[])"("mset", "MSET", .not_subscriber).call;
pub const msetnx = compile.@"(...strings: string[])"("msetnx", "MSETNX", .not_subscriber).call;
pub const script = compile.@"(...strings: string[])"("script", "SCRIPT", .not_subscriber).call;
pub const select = compile.@"(...strings: string[])"("select", "SELECT", .not_subscriber).call;
pub const spublish = compile.@"(...strings: string[])"("spublish", "SPUBLISH", .not_subscriber).call;
pub const smove = compile.@"(...strings: string[])"("smove", "SMOVE", .not_subscriber).call;
pub const substr = compile.@"(...strings: string[])"("substr", "SUBSTR", .not_subscriber).call;
pub const hstrlen = compile.@"(...strings: string[])"("hstrlen", "HSTRLEN", .not_subscriber).call;
pub const zrank = compile.@"(...strings: string[])"("zrank", "ZRANK", .not_subscriber).call;
pub const zrevrank = compile.@"(...strings: string[])"("zrevrank", "ZREVRANK", .not_subscriber).call;
pub const spublish = compile.@"(key: RedisKey, value: RedisValue)"("spublish", "SPUBLISH", "channel", "message", .not_subscriber).call;
pub fn smove(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
try requireNotSubscriber(this, @src().fn_name);
const source = (try fromJS(globalObject, callframe.argument(0))) orelse {
return globalObject.throwInvalidArgumentType("smove", "source", "string or buffer");
};
defer source.deinit();
const destination = (try fromJS(globalObject, callframe.argument(1))) orelse {
return globalObject.throwInvalidArgumentType("smove", "destination", "string or buffer");
};
defer destination.deinit();
const member = (try fromJS(globalObject, callframe.argument(2))) orelse {
return globalObject.throwInvalidArgumentType("smove", "member", "string or buffer");
};
defer member.deinit();
const promise = this.send(
globalObject,
callframe.this(),
&.{
.command = "SMOVE",
.args = .{ .args = &.{ source, destination, member } },
.meta = .{ .return_as_bool = true },
},
) catch |err| {
return protocol.valkeyErrorToJS(globalObject, "Failed to send SMOVE command", err);
};
return promise.toJS();
}
pub const substr = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("substr", "SUBSTR", "key", "start", "end", .not_subscriber).call;
pub const hstrlen = compile.@"(key: RedisKey, value: RedisValue)"("hstrlen", "HSTRLEN", "key", "field", .not_subscriber).call;
pub const zrank = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrank", "ZRANK", "key", .not_subscriber).call;
pub const zrangestore = compile.@"(...strings: string[])"("zrangestore", "ZRANGESTORE", .not_subscriber).call;
pub const zrem = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrem", "ZREM", "key", .not_subscriber).call;
pub const zremrangebylex = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zremrangebylex", "ZREMRANGEBYLEX", "key", "min", "max", .not_subscriber).call;
pub const zremrangebyrank = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zremrangebyrank", "ZREMRANGEBYRANK", "key", "start", "stop", .not_subscriber).call;
pub const zremrangebyscore = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zremrangebyscore", "ZREMRANGEBYSCORE", "key", "min", "max", .not_subscriber).call;
pub const zrevrank = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrevrank", "ZREVRANK", "key", .not_subscriber).call;
pub const psubscribe = compile.@"(...strings: string[])"("psubscribe", "PSUBSCRIBE", .dont_care).call;
pub const punsubscribe = compile.@"(...strings: string[])"("punsubscribe", "PUNSUBSCRIBE", .dont_care).call;
pub const pubsub = compile.@"(...strings: string[])"("pubsub", "PUBSUB", .dont_care).call;
pub const copy = compile.@"(...strings: string[])"("copy", "COPY", .not_subscriber).call;
pub const unlink = compile.@"(key: RedisKey, ...args: RedisKey[])"("unlink", "UNLINK", "key", .not_subscriber).call;
pub const touch = compile.@"(key: RedisKey, ...args: RedisKey[])"("touch", "TOUCH", "key", .not_subscriber).call;
pub const rename = compile.@"(key: RedisKey, value: RedisValue)"("rename", "RENAME", "key", "newkey", .not_subscriber).call;
pub const renamenx = compile.@"(key: RedisKey, value: RedisValue)"("renamenx", "RENAMENX", "key", "newkey", .not_subscriber).call;
pub fn publish(
this: *JSValkeyClient,
@@ -1004,6 +1309,30 @@ const compile = struct {
};
}
pub fn @"()"(
comptime name: []const u8,
comptime command: []const u8,
comptime client_state_requirement: ClientStateRequirement,
) type {
return struct {
pub fn call(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
try testCorrectState(this, name, client_state_requirement);
const promise = this.send(
globalObject,
callframe.this(),
&.{
.command = command,
.args = .{ .args = &.{} },
},
) catch |err| {
return protocol.valkeyErrorToJS(globalObject, "Failed to send " ++ command, err);
};
return promise.toJS();
}
};
}
pub fn @"(key: RedisKey)"(
comptime name: []const u8,
comptime command: []const u8,
@@ -1117,6 +1446,46 @@ const compile = struct {
};
}
pub fn @"(key: RedisKey, value: RedisValue, value2: RedisValue)"(
comptime name: []const u8,
comptime command: []const u8,
comptime arg0_name: []const u8,
comptime arg1_name: []const u8,
comptime arg2_name: []const u8,
comptime client_state_requirement: ClientStateRequirement,
) type {
return struct {
pub fn call(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
try testCorrectState(this, name, client_state_requirement);
const key = (try fromJS(globalObject, callframe.argument(0))) orelse {
return globalObject.throwInvalidArgumentType(name, arg0_name, "string or buffer");
};
defer key.deinit();
const value = (try fromJS(globalObject, callframe.argument(1))) orelse {
return globalObject.throwInvalidArgumentType(name, arg1_name, "string or buffer");
};
defer value.deinit();
const value2 = (try fromJS(globalObject, callframe.argument(2))) orelse {
return globalObject.throwInvalidArgumentType(name, arg2_name, "string or buffer");
};
defer value2.deinit();
const promise = this.send(
globalObject,
callframe.this(),
&.{
.command = command,
.args = .{ .args = &.{ key, value, value2 } },
},
) catch |err| {
return protocol.valkeyErrorToJS(globalObject, "Failed to send " ++ command, err);
};
return promise.toJS();
}
};
}
pub fn @"(...strings: string[])"(
comptime name: []const u8,
comptime command: []const u8,

View File

@@ -1,5 +1,5 @@
# Unified Dockerfile for Valkey/Redis with TCP, TLS, Unix socket, and authentication support
FROM redis:7-alpine
FROM redis:8-alpine
# Set user to root
USER root
@@ -10,6 +10,7 @@ RUN apk add --no-cache bash
# Create directories
RUN mkdir -p /etc/redis/certs
RUN mkdir -p /docker-entrypoint-initdb.d
RUN mkdir -p /data/appendonlydir && chown -R redis:redis /data
# Copy certificates
COPY server.key /etc/redis/certs/
@@ -25,7 +26,9 @@ RUN chmod +x /docker-entrypoint-initdb.d/init-redis.sh
# Expose ports
EXPOSE 6379 6380
WORKDIR /docker-entrypoint-initdb.d
WORKDIR /data
USER redis
# Use custom entrypoint to run initialization script
CMD ["redis-server", "/etc/redis/redis.conf"]

View File

@@ -1,349 +0,0 @@
import { beforeEach, describe, expect, test } from "bun:test";
import { ConnectionType, createClient, ctx, expectType, isEnabled } from "../test-utils";
/**
* Test suite covering Redis set operations
* - Basic operations (SADD, SREM, SISMEMBER)
* - Set retrieval (SMEMBERS, SCARD)
* - Set manipulation (SPOP, SRANDMEMBER)
* - Set operations (SUNION, SINTER, SDIFF)
*/
describe.skipIf(!isEnabled)("Valkey: Set Data Type Operations", () => {
beforeEach(() => {
if (ctx.redis?.connected) {
ctx.redis.close?.();
}
ctx.redis = createClient(ConnectionType.TCP);
});
describe("Basic Set Operations", () => {
test("SADD and SISMEMBER commands", async () => {
const key = ctx.generateKey("set-test");
// Add a single member
const singleAddResult = await ctx.redis.sadd(key, "member1");
console.log("singleAddResult", singleAddResult);
expectType<number>(singleAddResult, "number");
expect(singleAddResult).toBe(1); // 1 new member added
// Add multiple members using sendCommand
const multiAddResult = await ctx.redis.send("SADD", [key, "member2", "member3", "member1"]);
expectType<number>(multiAddResult, "number");
expect(multiAddResult).toBe(2); // 2 new members added, 1 duplicate ignored
// Check if member exists
const isFirstMember = await ctx.redis.sismember(key, "member1");
expect(isFirstMember).toBe(true);
// Check if non-existent member exists
const isNonMember = await ctx.redis.sismember(key, "nonexistent");
expect(isNonMember).toBe(false);
});
test("SREM command", async () => {
const key = ctx.generateKey("srem-test");
// Add multiple members
await ctx.redis.send("SADD", [key, "member1", "member2", "member3", "member4"]);
// Remove a single member
const singleRemResult = await ctx.redis.srem(key, "member1");
expectType<number>(singleRemResult, "number");
expect(singleRemResult).toBe(1); // 1 member removed
// Remove multiple members using sendCommand
const multiRemResult = await ctx.redis.send("SREM", [key, "member2", "member3", "nonexistent"]);
expectType<number>(multiRemResult, "number");
expect(multiRemResult).toBe(2); // 2 members removed, non-existent member ignored
// Verify only member4 remains
const members = await ctx.redis.smembers(key);
expect(Array.isArray(members)).toBe(true);
expect(members.length).toBe(1);
expect(members[0]).toBe("member4");
});
test("SMEMBERS command", async () => {
const key = ctx.generateKey("smembers-test");
// Add members one at a time using direct sadd method
await ctx.redis.sadd(key, "apple");
await ctx.redis.sadd(key, "banana");
await ctx.redis.sadd(key, "cherry");
// Get all members using direct smembers method
const members = await ctx.redis.smembers(key);
expect(Array.isArray(members)).toBe(true);
// Sort for consistent snapshot since set members can come in any order
const sortedMembers = [...members].sort();
expect(sortedMembers).toMatchInlineSnapshot(`
[
"apple",
"banana",
"cherry",
]
`);
});
test("SCARD command", async () => {
const key = ctx.generateKey("scard-test");
// Add members - using direct sadd method for first item, then send for multiple
await ctx.redis.sadd(key, "item1");
await ctx.redis.send("SADD", [key, "item2", "item3", "item4"]);
// Get set cardinality (size)
// TODO: When a direct scard method is implemented, use that instead
const size = await ctx.redis.send("SCARD", [key]);
expectType<number>(size, "number");
expect(size).toMatchInlineSnapshot(`4`);
// Remove some members - using direct srem method for first item, then send for second
await ctx.redis.srem(key, "item1");
await ctx.redis.send("SREM", [key, "item2"]);
// Check size again
const updatedSize = await ctx.redis.send("SCARD", [key]);
expectType<number>(updatedSize, "number");
expect(updatedSize).toMatchInlineSnapshot(`2`);
});
});
describe("Set Manipulation", () => {
test("SPOP command", async () => {
const key = ctx.generateKey("spop-test");
// Add members - using send for multiple values
// TODO: When a SADD method that supports multiple values is added, use that instead
await ctx.redis.send("SADD", [key, "red", "green", "blue", "yellow", "purple"]);
// Pop a single member - using direct spop method
const popResult = await ctx.redis.spop(key);
expect(popResult).toBeDefined();
expect(typeof popResult).toBe("string");
// Pop multiple members
// TODO: When SPOP method that supports count parameter is added, use that instead
const multiPopResult = await ctx.redis.send("SPOP", [key, "2"]);
expect(Array.isArray(multiPopResult)).toBe(true);
expect(multiPopResult.length).toMatchInlineSnapshot(`2`);
// Verify remaining members
// TODO: When a direct scard method is added, use that instead
const remainingCount = await ctx.redis.send("SCARD", [key]);
expectType<number>(remainingCount, "number");
expect(remainingCount).toMatchInlineSnapshot(`2`); // 5 original - 1 - 2 = 2 remaining
});
test("SRANDMEMBER command", async () => {
const key = ctx.generateKey("srandmember-test");
// Add members - first with direct sadd, then with send for remaining
await ctx.redis.sadd(key, "one");
await ctx.redis.send("SADD", [key, "two", "three", "four", "five"]);
// Get a random member - using direct srandmember method
const randResult = await ctx.redis.srandmember(key);
expect(randResult).toBeDefined();
expect(typeof randResult).toBe("string");
// Get multiple random members
// TODO: When srandmember method with count parameter is added, use that instead
const multiRandResult = await ctx.redis.send("SRANDMEMBER", [key, "3"]);
expect(Array.isArray(multiRandResult)).toBe(true);
expect(multiRandResult.length).toMatchInlineSnapshot(`3`);
// Verify set is unchanged
const count = await ctx.redis.send("SCARD", [key]);
expectType<number>(count, "number");
expect(count).toMatchInlineSnapshot(`5`); // All members still present unlike SPOP
});
test("SMOVE command", async () => {
const sourceKey = ctx.generateKey("smove-source");
const destinationKey = ctx.generateKey("smove-dest");
// Set up source and destination sets
await ctx.redis.send("SADD", [sourceKey, "a", "b", "c"]);
await ctx.redis.send("SADD", [destinationKey, "c", "d", "e"]);
// Move a member from source to destination
const moveResult = await ctx.redis.send("SMOVE", [sourceKey, destinationKey, "b"]);
expectType<number>(moveResult, "number");
expect(moveResult).toBe(1); // 1 indicates success
// Try to move a non-existent member
const failedMoveResult = await ctx.redis.send("SMOVE", [sourceKey, destinationKey, "z"]);
expectType<number>(failedMoveResult, "number");
expect(failedMoveResult).toBe(0); // 0 indicates failure
// Verify source set (should have "a" and "c" left)
const sourceMembers = await ctx.redis.smembers(sourceKey);
expect(Array.isArray(sourceMembers)).toBe(true);
expect(sourceMembers.length).toBe(2);
expect(sourceMembers).toContain("a");
expect(sourceMembers).toContain("c");
expect(sourceMembers).not.toContain("b");
// Verify destination set (should have "b", "c", "d", "e")
const destMembers = await ctx.redis.smembers(destinationKey);
expect(Array.isArray(destMembers)).toBe(true);
expect(destMembers.length).toBe(4);
expect(destMembers).toContain("b");
expect(destMembers).toContain("c");
expect(destMembers).toContain("d");
expect(destMembers).toContain("e");
});
});
describe("Set Operations", () => {
test("SUNION and SUNIONSTORE commands", async () => {
const set1 = ctx.generateKey("sunion-1");
const set2 = ctx.generateKey("sunion-2");
const set3 = ctx.generateKey("sunion-3");
const destSet = ctx.generateKey("sunion-dest");
// Set up test sets
await ctx.redis.send("SADD", [set1, "a", "b", "c"]);
await ctx.redis.send("SADD", [set2, "c", "d", "e"]);
await ctx.redis.send("SADD", [set3, "e", "f", "g"]);
// Get union of two sets
const unionResult = await ctx.redis.send("SUNION", [set1, set2]);
expect(Array.isArray(unionResult)).toBe(true);
expect(unionResult.length).toBe(5);
expect(unionResult).toContain("a");
expect(unionResult).toContain("b");
expect(unionResult).toContain("c");
expect(unionResult).toContain("d");
expect(unionResult).toContain("e");
// Store union of three sets
const storeResult = await ctx.redis.send("SUNIONSTORE", [destSet, set1, set2, set3]);
expectType<number>(storeResult, "number");
expect(storeResult).toBe(7); // 7 unique members across all sets
// Verify destination set
const destMembers = await ctx.redis.smembers(destSet);
expect(Array.isArray(destMembers)).toBe(true);
expect(destMembers.length).toBe(7);
expect(destMembers).toContain("a");
expect(destMembers).toContain("b");
expect(destMembers).toContain("c");
expect(destMembers).toContain("d");
expect(destMembers).toContain("e");
expect(destMembers).toContain("f");
expect(destMembers).toContain("g");
});
test("SINTER and SINTERSTORE commands", async () => {
const set1 = ctx.generateKey("sinter-1");
const set2 = ctx.generateKey("sinter-2");
const set3 = ctx.generateKey("sinter-3");
const destSet = ctx.generateKey("sinter-dest");
// Set up test sets
await ctx.redis.send("SADD", [set1, "a", "b", "c", "d"]);
await ctx.redis.send("SADD", [set2, "c", "d", "e"]);
await ctx.redis.send("SADD", [set3, "a", "c", "e"]);
// Get intersection of two sets
const interResult = await ctx.redis.send("SINTER", [set1, set2]);
expect(Array.isArray(interResult)).toBe(true);
expect(interResult.length).toBe(2);
expect(interResult).toContain("c");
expect(interResult).toContain("d");
// Store intersection of three sets
const storeResult = await ctx.redis.send("SINTERSTORE", [destSet, set1, set2, set3]);
expectType<number>(storeResult, "number");
expect(storeResult).toBe(1); // Only "c" is in all three sets
// Verify destination set
const destMembers = await ctx.redis.smembers(destSet);
expect(Array.isArray(destMembers)).toBe(true);
expect(destMembers.length).toBe(1);
expect(destMembers[0]).toBe("c");
});
test("SDIFF and SDIFFSTORE commands", async () => {
const set1 = ctx.generateKey("sdiff-1");
const set2 = ctx.generateKey("sdiff-2");
const destSet = ctx.generateKey("sdiff-dest");
// Set up test sets
await ctx.redis.send("SADD", [set1, "a", "b", "c", "d"]);
await ctx.redis.send("SADD", [set2, "c", "d", "e"]);
// Get difference (elements in set1 that aren't in set2)
const diffResult = await ctx.redis.send("SDIFF", [set1, set2]);
expect(Array.isArray(diffResult)).toBe(true);
expect(diffResult.length).toBe(2);
expect(diffResult).toContain("a");
expect(diffResult).toContain("b");
// Store difference
const storeResult = await ctx.redis.send("SDIFFSTORE", [destSet, set1, set2]);
expectType<number>(storeResult, "number");
expect(storeResult).toBe(2); // "a" and "b" are only in set1
// Verify destination set
const destMembers = await ctx.redis.smembers(destSet);
expect(Array.isArray(destMembers)).toBe(true);
expect(destMembers.length).toBe(2);
expect(destMembers).toContain("a");
expect(destMembers).toContain("b");
});
});
describe("Scanning Operations", () => {
test("SSCAN command", async () => {
const key = ctx.generateKey("sscan-test");
// Create a set with many members
const memberCount = 100;
const members = [];
for (let i = 0; i < memberCount; i++) {
members.push(`member:${i}`);
}
await ctx.redis.send("SADD", [key, ...members]);
// Use SSCAN to iterate through members
const scanResult = await ctx.redis.send("SSCAN", [key, "0", "COUNT", "20"]);
expect(Array.isArray(scanResult)).toBe(true);
expect(scanResult.length).toBe(2);
const cursor = scanResult[0];
const items = scanResult[1];
// Cursor should be either "0" (done) or a string number
expect(typeof cursor).toBe("string");
// Items should be an array of members
expect(Array.isArray(items)).toBe(true);
// All results should match our expected pattern
for (const item of items) {
expect(item.startsWith("member:")).toBe(true);
}
// Verify MATCH pattern works
const patternResult = await ctx.redis.send("SSCAN", [key, "0", "MATCH", "member:1*", "COUNT", "1000"]);
expect(Array.isArray(patternResult)).toBe(true);
expect(patternResult.length).toBe(2);
const patternItems = patternResult[1];
expect(Array.isArray(patternItems)).toBe(true);
// Should return only members that match the pattern (member:1, member:10-19, etc)
// There should be at least "member:1" and "member:10" through "member:19"
expect(patternItems.length).toBeGreaterThan(0);
for (const item of patternItems) {
expect(item.startsWith("member:1")).toBe(true);
}
});
});
});

File diff suppressed because it is too large Load Diff