From 9f9b4414694a9ec3f71d57c94b6cfc9c1eebe6c2 Mon Sep 17 00:00:00 2001 From: Rushabh Nilesh Kothari <77202623+rushabhk04@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:27:25 -0500 Subject: [PATCH] #761: Added ZADD options implementation - XX|NX|CH|INCR| LT|GT according to Redis source code (#1262) --- integration_tests/commands/http/zset_test.go | 713 ++++++++++++++++++ integration_tests/commands/resp/zset_test.go | 536 +++++++++++++ .../commands/websocket/zset_test.go | 538 +++++++++++++ internal/eval/constants.go | 2 + internal/eval/eval.go | 4 +- internal/eval/store_eval.go | 173 ++++- internal/store/constants.go | 14 +- internal/watchmanager/watch_manager.go | 12 +- 8 files changed, 1957 insertions(+), 35 deletions(-) diff --git a/integration_tests/commands/http/zset_test.go b/integration_tests/commands/http/zset_test.go index ed5a1f293..67e23ce01 100644 --- a/integration_tests/commands/http/zset_test.go +++ b/integration_tests/commands/http/zset_test.go @@ -200,3 +200,716 @@ func TestZCOUNT(t *testing.T) { }) } } + +func TestZADD(t *testing.T) { + exec := NewHTTPCommandExecutor() + testCases := []struct { + name string + commands []HTTPCommand + expected []interface{} + }{ + { + name: "ZADD with two new members", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"1", "member1", "2", "member2"}}}, + }, + expected: []interface{}{float64(2)}, + }, + { + name: "ZADD with three new members", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"3", "member3", "4", "member4", "5", "member5"}}}, + }, + expected: []interface{}{float64(3)}, + }, + { + name: "ZADD with existing members", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"1", "member1", "2", "member2", "3", "member3", "4", "member4", "5", "member5"}}}, + }, + expected: []interface{}{float64(5)}, + }, + { + name: "ZADD with mixed new and existing members", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"1", "member1", "2", "member2", "3", "member3", "4", "member4", "5", "member5", "6", "member6"}}}, + }, + expected: []interface{}{float64(6)}, + }, + { + name: "ZADD without any members", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"1"}}}, + }, + expected: []interface{}{"ERR wrong number of arguments for 'zadd' command"}, + }, + + { + name: "ZADD XX option without existing key", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"XX", "10", "member9"}}}, + }, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD XX with existing key and member2", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"XX", "20", "member2"}}}, + }, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD XX updates existing elements scores", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"XX", "15", "member1", "25", "member2"}}}, + }, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD GT and XX only updates existing elements when new scores are greater", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"GT", "XX", "20", "member1"}}}, + }, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD LT and XX only updates existing elements when new scores are lower", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"LT", "XX", "20", "member1"}}}, + }, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD NX and XX not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "XX", "20", "member1"}}}, + }, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD XX and CH compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"XX", "CH", "20", "member1"}}}, + }, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD INCR and XX compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"XX", "INCR", "20", "member1"}}}, + }, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD INCR and XX not compatible because of more than one member", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"XX", "INCR", "20", "member1", "25", "member2"}}}, + }, + expected: []interface{}{"ERR incr option supports a single increment-element pair"}, + }, + { + name: "ZADD XX, LT and GT are not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"XX", "LT", "GT", "20", "member1"}}}, + }, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD XX, LT, GT, CH, INCR are not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"XX", "LT", "GT", "INCR", "CH", "20", "member1"}}}, + }, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD XX, GT and CH compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"XX", "GT", "CH", "60", "member1", "30", "member2"}}}, + }, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD XX, LT and CH compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"XX", "LT", "CH", "4", "member1", "1", "member2"}}}, + }, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD XX with existing key and new member", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"XX", "20", "member20"}}}, + }, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD XX wont update as new members", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"XX", "15", "member18", "25", "member20"}}}, + }, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD XX and GT wont add new member", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"GT", "XX", "20", "member18"}}}, + }, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD XX and LT and new member wont update", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"LT", "XX", "20", "member18"}}}, + }, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD XX and CH and new member wont work", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"XX", "CH", "20", "member18"}}}, + }, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD XX, LT, CH, new member wont update", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"XX", "LT", "CH", "50", "member18", "40", "member20"}}}, + }, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD XX, GT and CH, new member wont update", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"XX", "GT", "CH", "60", "member18", "30", "member20"}}}, + }, + expected: []interface{}{float64(0)}, + }, + + { + name: "ZADD NX existing key new member", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "10", "member9"}}}, + }, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD NX existing key old member", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "20", "member2"}}}, + }, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD NX existing key one new member and one old member", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "15", "member1", "25", "member11"}}}, + }, + expected: []interface{}{float64(2)}, + }, + { + name: "ZADD NX and XX not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "XX", "20", "member1"}}}, + }, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX CH not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "XX", "CH", "20", "member1"}}}, + }, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX CH INCR not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "XX", "CH", "INCR", "20", "member1"}}}, + }, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX LT not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "XX", "LT", "20", "member1"}}}, + }, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX GT not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "XX", "GT", "20", "member1"}}}, + }, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX LT CH not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "XX", "LT", "CH", "20", "member1"}}}, + }, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX LT CH INCR not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "XX", "LT", "CH", "INCR", "20", "member1"}}}, + }, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX GT CH not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "XX", "GT", "CH", "20", "member1"}}}, + }, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX GT CH INCR not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "XX", "GT", "CH", "INCR", "20", "member1"}}}, + }, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX INCR not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "XX", "INCR", "20", "member1"}}}, + }, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX INCR LT not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "XX", "INCR", "LT", "20", "member1"}}}, + }, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX INCR GT not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "XX", "INCR", "GT", "20", "member1"}}}, + }, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX LT GT not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "XX", "LT", "GT", "20", "member1"}}}, + }, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX LT GT CH not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "XX", "LT", "GT", "CH", "20", "member1"}}}, + }, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX LT GT CH INCR not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "XX", "LT", "GT", "CH", "INCR", "20", "member1"}}}, + }, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + + // NX without XX and all LT GT CH and INCR - all errors + { + name: "ZADD NX and GT incompatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "GT", "20", "member1"}}}, + }, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX and LT incompatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "LT", "20", "member1"}}}, + }, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT and GT incompatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "LT", "GT", "20", "member1"}}}, + }, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT, GT and INCR incompatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "LT", "GT", "INCR", "20", "member1"}}}, + }, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT, GT and CH incompatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "LT", "GT", "CH", "20", "member1"}}}, + }, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT, GT, CH and INCR incompatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "LT", "GT", "CH", "INCR", "20", "member1"}}}, + }, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT, CH not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "LT", "CH", "20", "member1"}}}, + }, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT, INCR not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "LT", "INCR", "20", "member1"}}}, + }, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT, CH, INCR not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "LT", "CH", "INCR", "20", "member1"}}}, + }, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, GT, CH not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "GT", "CH", "20", "member1"}}}, + }, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, GT, INCR not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "GT", "INCR", "20", "member1"}}}, + }, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, GT, CH, INCR not compatible", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "GT", "CH", "INCR", "20", "member1"}}}, + }, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, CH with new member returns CH based - if added or not", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "CH", "20", "member13"}}}, + }, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD NX, CH with existing member returns CH based - if added or not", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"NX", "CH", "10", "member13"}}}, + }, + expected: []interface{}{float64(1)}, + }, + + { + name: "ZADD with GT with existing member", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"GT", "15", "member14"}}}, + }, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD with GT with new member", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"GT", "15", "member15"}}}, + }, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD GT and LT", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"GT", "LT", "15", "member15"}}}, + }, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD GT LT CH", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"GT", "LT", "CH", "15", "member15"}}}, + }, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD GT LT CH INCR", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"GT", "LT", "CH", "INCR", "15", "member15"}}}, + }, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD GT LT INCR", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"GT", "LT", "INCR", "15", "member15"}}}, + }, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD GT CH with existing member score less no change hence 0", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"GT", "CH", "10", "member15"}}}, + }, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD GT CH with existing member score more, changed score hence 1", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"GT", "CH", "25", "member15"}}}, + }, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD GT CH with existing member score equal, nothing returned", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"GT", "CH", "25", "member15"}}}, + }, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD GT CH with new member score", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"GT", "CH", "5", "member19"}}}, + }, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD GT with INCR if score less than current score after INCR returns nil", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"GT", "INCR", "-5", "member15"}}}, + }, + expected: []interface{}{float64(-5)}, + }, + { + name: "ZADD GT with INCR updates existing member score if greater after INCR", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"GT", "INCR", "5", "member15"}}}, + }, + expected: []interface{}{float64(5)}, + }, + + // ZADD with LT options + { + name: "ZADD with LT with existing member score greater", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"LT", "15", "member14"}}}, + }, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD with LT with new member", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"LT", "15", "member23"}}}, + }, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD LT with existing member score equal", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"LT", "15", "member14"}}}, + }, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD LT with existing member score less", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"LT", "10", "member14"}}}, + }, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD LT with INCR does not update if score is greater after INCR", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"LT", "INCR", "5", "member14"}}}, + }, + expected: []interface{}{float64(5)}, + }, + { + name: "ZADD LT with INCR updates if updated score is less than current", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"LT", "INCR", "-1", "member14"}}}, + }, + expected: []interface{}{float64(-1)}, + }, + { + name: "ZADD LT with CH updates existing member score if less, CH returns changed elements", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"LT", "CH", "5", "member1", "2", "member2"}}}, + }, + expected: []interface{}{float64(2)}, + }, + // ZADD with INCR options + { + name: "ZADD INCR with new members, inserts as it is", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"INCR", "15", "member24"}}}, + }, + expected: []interface{}{float64(15)}, + }, + { + name: "ZADD INCR with existing members, increases the score", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"INCR", "5", "member24"}}}, + }, + expected: []interface{}{float64(5)}, + }, + // ZADD with CH options + { + name: "ZADD CH with one existing member update, returns count of updates", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"CH", "45", "member2"}}}, + }, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD CH with multiple existing member updates, returns count of updates", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"CH", "50", "member2", "63", "member3"}}}, + }, + expected: []interface{}{float64(2)}, + }, + { + name: "ZADD CH with 1 new and 1 existing member update, returns count of updates", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "myzset2", "values": [...]string{"CH", "50", "member2", "64", "member32"}}}, + }, + expected: []interface{}{float64(2)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + exec.FireCommand(HTTPCommand{ + Command: "DEL", + Body: map[string]interface{}{"key": "myzset2"}, + }) + + t.Cleanup(func() { + exec.FireCommand(HTTPCommand{ + Command: "DEL", + Body: map[string]interface{}{"key": "myzset2"}, + }) + t.Log("Pre-test cleanup executed: Deleted key 'myzset2'") + }) + + // Execute test commands and validate results + for i, cmd := range tc.commands { + result, _ := exec.FireCommand(cmd) + assert.Equal(t, tc.expected[i], result) + } + }) + } +} + +func TestZRANGE_HTTP(t *testing.T) { + exec := NewHTTPCommandExecutor() + + testCases := []struct { + name string + commands []HTTPCommand + expected []interface{} + }{ + { + name: "ZRANGE with mixed indices", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "key", "values": [...]string{"1", "member1", "2", "member2", "3", "member3", "4", "member4", "5", "member5", "6", "member6"}}}, + {Command: "ZRANGE", Body: map[string]interface{}{"key": "key", "values": [...]string{"0", "-1"}}}, // Use start and stop instead of values + }, + expected: []interface{}{float64(6), []interface{}{"member1", "member2", "member3", "member4", "member5", "member6"}}, + }, + { + name: "ZRANGE with positive indices #1", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "key", "values": []string{"1", "member1", "2", "member2", "3", "member3", "4", "member4", "5", "member5", "6", "member6"}}}, + {Command: "ZRANGE", Body: map[string]interface{}{"key": "key", "values": [...]string{"0", "2"}}}, + }, + expected: []interface{}{float64(6), []interface{}{"member1", "member2", "member3"}}, + }, + { + name: "ZRANGE with positive indices #2", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "key", "values": []string{"1", "member1", "2", "member2", "3", "member3", "4", "member4", "5", "member5", "6", "member6"}}}, + {Command: "ZRANGE", Body: map[string]interface{}{"key": "key", "values": [...]string{"2", "4"}}}, + }, + expected: []interface{}{float64(6), []interface{}{"member3", "member4", "member5"}}, + }, + { + name: "ZRANGE with all positive indices", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "key", "values": []string{"1", "member1", "2", "member2", "3", "member3", "4", "member4", "5", "member5", "6", "member6"}}}, + {Command: "ZRANGE", Body: map[string]interface{}{"key": "key", "values": [...]string{"0", "10"}}}, + }, + expected: []interface{}{float64(6), []interface{}{"member1", "member2", "member3", "member4", "member5", "member6"}}, + }, + { + name: "ZRANGE with out of bound indices", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "key", "values": []string{"1", "member1", "2", "member2", "3", "member3", "4", "member4", "5", "member5", "6", "member6"}}}, + {Command: "ZRANGE", Body: map[string]interface{}{"key": "key", "values": [...]string{"10", "20"}}}, + }, + expected: []interface{}{float64(6), []interface{}{}}, + }, + { + name: "ZRANGE with positive indices and scores", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "key", "values": []string{"1", "member1", "2", "member2", "3", "member3", "4", "member4", "5", "member5", "6", "member6"}}}, + {Command: "ZRANGE", Body: map[string]interface{}{"key": "key", "values": [...]string{"0", "10", "WITHSCORES"}}}, + }, + expected: []interface{}{float64(6), []interface{}{"member1", "1", "member2", "2", "member3", "3", "member4", "4", "member5", "5", "member6", "6"}}, + }, + { + name: "ZRANGE with positive indices and scores in reverse order", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "key", "values": []string{"1", "member1", "2", "member2", "3", "member3", "4", "member4", "5", "member5", "6", "member6"}}}, + {Command: "ZRANGE", Body: map[string]interface{}{"key": "key", "values": [...]string{"0", "10", "REV", "WITHSCORES"}}}, + }, + expected: []interface{}{float64(6), []interface{}{"member6", "6", "member5", "5", "member4", "4", "member3", "3", "member2", "2", "member1", "1"}}, + }, + { + name: "ZRANGE with negative indices", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "key", "values": []string{"1", "member1", "2", "member2", "3", "member3", "4", "member4", "5", "member5", "6", "member6"}}}, + {Command: "ZRANGE", Body: map[string]interface{}{"key": "key", "values": [...]string{"-1", "-1"}}}, + }, + expected: []interface{}{float64(6), []interface{}{"member6"}}, + }, + { + name: "ZRANGE with negative indices and scores", + commands: []HTTPCommand{ + {Command: "ZADD", Body: map[string]interface{}{"key": "key", "values": []string{"1", "member1", "2", "member2", "3", "member3", "4", "member4", "5", "member5", "6", "member6"}}}, + {Command: "ZRANGE", Body: map[string]interface{}{"key": "key", "values": [...]string{"-8", "-5", "WITHSCORES"}}}, + }, + expected: []interface{}{float64(6), []interface{}{"member1", "1", "member2", "2"}}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Clear previous data + exec.FireCommand(HTTPCommand{ + Command: "DEL", + Body: map[string]interface{}{"key": "key"}, + }) + + // Execute commands and validate results + for i, cmd := range tc.commands { + result, err := exec.FireCommand(cmd) + if err != nil { + t.Fatalf("Error executing command %v: %v", cmd, err) + } + + // Check if the result matches the expected value + assert.Equal(t, tc.expected[i], result) + } + }) + } +} diff --git a/integration_tests/commands/resp/zset_test.go b/integration_tests/commands/resp/zset_test.go index be6e3a9a3..9f246d9ed 100644 --- a/integration_tests/commands/resp/zset_test.go +++ b/integration_tests/commands/resp/zset_test.go @@ -127,3 +127,539 @@ func TestZCOUNT(t *testing.T) { }) } } + +func TestZADD(t *testing.T) { + conn := getLocalConnection() + defer conn.Close() + + testCases := []TestCase{ + { + name: "ZADD with two new members", + commands: []string{"ZADD key 1 member1 2 member2"}, + expected: []interface{}{int64(2)}, + }, + { + name: "ZADD with three new members", + commands: []string{"ZADD key 3 member3 4 member4 5 member5"}, + expected: []interface{}{int64(3)}, + }, + { + name: "ZADD with existing members", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5"}, + expected: []interface{}{int64(5)}, + }, + { + name: "ZADD with mixed new and existing members", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6"}, + expected: []interface{}{int64(6)}, + }, + { + name: "ZADD without any members", + commands: []string{"ZADD key 1"}, + expected: []interface{}{"ERR wrong number of arguments for 'zadd' command"}, + }, + + // *************************************** ZADD with XX options validation starts now, including XX with GT, LT, NX, INCR, CH ************************** + { + name: "ZADD XX option without existing key", + commands: []string{"ZADD key XX 10 member9"}, + expected: []interface{}{int64(0)}, + }, + { + name: "ZADD XX with existing key and member2", + commands: []string{"ZADD key XX 20 member2"}, + expected: []interface{}{int64(0)}, + }, + { + name: "ZADD XX updates existing elements scores", + commands: []string{"ZADD key XX 15 member1 25 member2"}, + expected: []interface{}{int64(0)}, + }, + { + name: "ZADD GT and XX only updates existing elements when new scores are greater", + commands: []string{"ZADD key GT XX 20 member1"}, + expected: []interface{}{int64(0)}, + }, + { + name: "ZADD LT and XX only updates existing elements when new scores are lower", + commands: []string{"ZADD key LT XX 20 member1"}, + expected: []interface{}{int64(0)}, + }, + { + name: "ZADD NX and XX not compatible", + commands: []string{"ZADD key NX XX 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD XX and CH compatible", + commands: []string{"ZADD key XX CH 20 member1"}, + expected: []interface{}{int64(0)}, + }, + { + name: "ZADD INCR and XX compatible", + commands: []string{"ZADD key XX INCR 20 member1"}, + expected: []interface{}{int64(0)}, + }, + { + name: "ZADD INCR and XX not compatible because of more than one member", + commands: []string{"ZADD key XX INCR 20 member1 25 member2"}, + expected: []interface{}{"ERR incr option supports a single increment-element pair"}, + }, + + { + name: "ZADD XX, LT and GT are not compatible", + commands: []string{"ZADD key XX LT GT 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD XX, LT, GT, CH, INCR are not compatible", + commands: []string{"ZADD key XX LT GT INCR CH 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + + { + name: "ZADD XX, GT and CH compatible", + commands: []string{"ZADD key XX GT CH 60 member1 30 member2"}, + expected: []interface{}{int64(0)}, + }, + + { + name: "ZADD XX, LT and CH compatible", + commands: []string{"ZADD key XX LT CH 4 member1 1 member2"}, + expected: []interface{}{int64(0)}, + }, + + //running with new members, XX wont update with new members, only existing gets updated + + { + name: "ZADD XX with existing key and new member", + commands: []string{"ZADD key XX 20 member20"}, + expected: []interface{}{int64(0)}, + }, + { + name: "ZADD XX wont update as new members", + commands: []string{"ZADD key XX 15 member18 25 member20"}, + expected: []interface{}{int64(0)}, + }, + { + name: "ZADD XX and GT wont add new member", + commands: []string{"ZADD key GT XX 20 member18"}, + expected: []interface{}{int64(0)}, + }, + { + name: "ZADD XX and LT and new member wont update", + commands: []string{"ZADD key LT XX 20 member18"}, + expected: []interface{}{int64(0)}, + }, + { + name: "ZADD XX and CH and new member wont work", + commands: []string{"ZADD key XX CH 20 member18"}, + expected: []interface{}{int64(0)}, + }, + + { + name: "ZADD XX, LT, CH, new member wont update", + commands: []string{"ZADD key XX LT CH 50 member18 40 member20"}, + expected: []interface{}{int64(0)}, + }, + { + name: "ZADD XX, GT and CH, new member wont update", + commands: []string{"ZADD key XX GT CH 60 member18 30 member20"}, + expected: []interface{}{int64(0)}, + }, + + // ******************************************* ZADD with NX starts now, including GT, LT, XX, INCR, CH *************** + + { + name: "ZADD NX existing key new member", + commands: []string{"ZADD key NX 10 member9"}, + expected: []interface{}{int64(1)}, + }, + { + name: "ZADD NX existing key old member", + commands: []string{"ZADD key NX 20 member2"}, + expected: []interface{}{int64(1)}, + }, + { + name: "ZADD NX existing key one new member and one old member", + commands: []string{"ZADD key NX 15 member1 25 member11"}, + expected: []interface{}{int64(2)}, + }, + + // NX and XX with all LT GT CH and INCR all errors + { + name: "ZADD NX and XX not compatible", + commands: []string{"ZADD key NX XX 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX CH not compatible", + commands: []string{"ZADD key NX XX CH 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX CH INCR not compatible", + commands: []string{"ZADD key NX XX CH INCR 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX LT not compatible", + commands: []string{"ZADD key NX XX LT 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX GT not compatible", + commands: []string{"ZADD key NX XX GT 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX LT CH not compatible", + commands: []string{"ZADD key NX XX LT CH 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX LT CH INCR compatible", + commands: []string{"ZADD key NX XX LT CH INCR 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX GT CH not compatible", + commands: []string{"ZADD key NX XX GT CH 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX GT CH INCR not compatible", + commands: []string{"ZADD key NX XX GT CH INCR 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX INCR not compatible", + commands: []string{"ZADD key NX XX INCR 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX INCR LT not compatible", + commands: []string{"ZADD key NX XX INCR LT 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX INCR GT not compatible", + commands: []string{"ZADD key NX XX INCR GT 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX LT GT not compatible", + commands: []string{"ZADD key NX XX LT GT 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX LT GT CH not compatible", + commands: []string{"ZADD key NX XX LT GT CH 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX LT GT CH INCR not compatible", + commands: []string{"ZADD key NX XX LT GT CH INCR 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + + // NX without XX and all LT GT CH and INCR // all are error + { + name: "ZADD NX and GT incompatible", + commands: []string{"ZADD key NX GT 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX and LT incompatible", + commands: []string{"ZADD key NX LT 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT and GT incompatible", + commands: []string{"ZADD key NX LT GT 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT, GT and INCR incompatible", + commands: []string{"ZADD key NX LT GT INCR 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT, GT and CH incompatible", + commands: []string{"ZADD key NX LT GT CH 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT, GT, CH and INCR incompatible", + commands: []string{"ZADD key NX LT GT CH INCR 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT, CH not compatible", + commands: []string{"ZADD key NX LT CH 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT, INCR not compatible", + commands: []string{"ZADD key NX LT INCR 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT, CH, INCR not compatible", + commands: []string{"ZADD key NX LT CH INCR 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, GT, CH not compatible", + commands: []string{"ZADD key NX GT CH 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, GT, INCR not compatible", + commands: []string{"ZADD key NX GT INCR 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, GT, CH, INCR not compatible", + commands: []string{"ZADD key NX GT CH INCR 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + + { + name: "ZADD NX, CH with new member returns CH based - if added or not", + commands: []string{"ZADD key NX CH 20 member13"}, + expected: []interface{}{int64(1)}, + }, + { + name: "ZADD NX, CH with existing member returns CH based - if added or not", + commands: []string{"ZADD key NX CH 10 member13"}, + expected: []interface{}{int64(1)}, + }, + + // *************************************** ZADD with GT options validation starts now, including GT with XX, LT, NX, INCR, CH ************************** + + { + name: "ZADD with GT with existing member", + commands: []string{"ZADD key GT 15 member14"}, + expected: []interface{}{int64(1)}, + }, + { + name: "ZADD with GT with new member", + commands: []string{"ZADD key GT 15 member15"}, + expected: []interface{}{int64(1)}, + }, + { + name: "ZADD GT and LT", + commands: []string{"ZADD key GT LT 15 member15"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD GT LT CH", + commands: []string{"ZADD key GT LT CH 15 member15"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD GT LT CH INCR", + commands: []string{"ZADD key GT LT CH INCR 15 member15"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD GT LT INCR", + commands: []string{"ZADD key GT LT INCR 15 member15"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD GT CH with existing member score less no change hence 0", + commands: []string{"ZADD key GT CH 10 member15"}, + expected: []interface{}{int64(1)}, + }, + { + name: "ZADD GT CH with existing member score more, changed score hence 1", + commands: []string{"ZADD key GT CH 25 member15"}, + expected: []interface{}{int64(1)}, + }, + { + name: "ZADD GT CH with existing member score equal, nothing returned", + commands: []string{"ZADD key GT CH 25 member15"}, + expected: []interface{}{int64(1)}, + }, + { + name: "ZADD GT CH with new member score", + commands: []string{"ZADD key GT CH 5 member19"}, + expected: []interface{}{int64(1)}, + }, + { + name: "ZADD GT with INCR if score less than currentscore after INCR returns nil", + commands: []string{"ZADD key GT INCR -5 member15"}, + expected: []interface{}{int64(-5)}, + }, + { + name: "ZADD GT with INCR updates existing member score if greater after INCR", + commands: []string{"ZADD key GT INCR 5 member15"}, + expected: []interface{}{int64(5)}, + }, + + // *************************************** ZADD with LT options validation starts now, including LT with GT, XX, NX, INCR, CH ************************** + + { + name: "ZADD with LT with existing member score greater", + commands: []string{"ZADD key LT 15 member14"}, + expected: []interface{}{int64(1)}, + }, + + { + name: "ZADD with LT with new member", + commands: []string{"ZADD key LT 15 member23"}, + expected: []interface{}{int64(1)}, + }, + + { + name: "ZADD LT with existing member score equal", + commands: []string{"ZADD key LT 15 member14"}, + expected: []interface{}{int64(1)}, + }, + + { + name: "ZADD LT with existing member score less", + commands: []string{"ZADD key LT 10 member14"}, + expected: []interface{}{int64(1)}, + }, + + { + name: "ZADD LT with INCR not updates existing member as score is greater after INCR", + commands: []string{"ZADD key LT INCR 5 member14"}, + expected: []interface{}{int64(5)}, + }, + + { + name: "ZADD LT with INCR updates existing member as updatedscore after INCR is less than current", + commands: []string{"ZADD key LT INCR -1 member14"}, + expected: []interface{}{int64(-1)}, + }, + + { + name: "ZADD LT with CH updates existing member score if less, CH returns changed elements", + commands: []string{"ZADD key LT CH 5 member1 2 member2"}, + expected: []interface{}{int64(2)}, + }, + + // *************************************** ZADD with INCR options validation starts now, including INCR with GT, LT, NX, XX, CH ************************** + { + name: "ZADD INCR with new members, insert as it is ", + commands: []string{"ZADD key INCR 15 member24"}, + expected: []interface{}{int64(15)}, + }, + + { + name: "ZADD INCR with existing members, increase the score", + commands: []string{"ZADD key INCR 5 member24"}, + expected: []interface{}{int64(5)}, + }, + + // *************************************** ZADD with CH options validation starts now, including CH with GT, LT, NX, XX, INCR ************************** + { + name: "ZADD CH with one existing members update, returns count of updation", + commands: []string{"ZADD key CH 45 member2"}, + expected: []interface{}{int64(1)}, + }, + + { + name: "ZADD CH with multiple existing members update, returns count of updation", + commands: []string{"ZADD key CH 50 member2 63 member3"}, + expected: []interface{}{int64(2)}, + }, + + { + name: "ZADD CH with 1 new and 1 existing member update, returns count of updation", + commands: []string{"ZADD key CH 50 member2 64 member32"}, + expected: []interface{}{int64(2)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + FireCommand(conn, "DEL key") // Resetting the key before each test + + // post cleanup + t.Cleanup(func() { + FireCommand(conn, "DEL key") + }) + + for i, cmd := range tc.commands { + result := FireCommand(conn, cmd) + assert.Equal(t, tc.expected[i], result) + } + }) + } +} + +func TestZRANGE(t *testing.T) { + conn := getLocalConnection() + defer conn.Close() + + print("In zrange") + + testCases := []TestCase{ + { + name: "ZRANGE with mixed indices", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6", "ZRANGE key 0 -1"}, + expected: []interface{}{int64(6), []interface{}{"member1", "member2", "member3", "member4", "member5", "member6"}}, + }, + { + name: "ZRANGE with positive indices #1", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6", "ZRANGE key 0 2"}, + expected: []interface{}{int64(6), []interface{}{"member1", "member2", "member3"}}, + }, + { + name: "ZRANGE with positive indices #2", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6", "ZRANGE key 2 4"}, + expected: []interface{}{int64(6), []interface{}{"member3", "member4", "member5"}}, + }, + { + name: "ZRANGE with all positive indices", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6", "ZRANGE key 0 10"}, + expected: []interface{}{int64(6), []interface{}{"member1", "member2", "member3", "member4", "member5", "member6"}}, + }, + { + name: "ZRANGE with out of bound indices", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6", "ZRANGE key 10 20"}, + expected: []interface{}{int64(6), []interface{}{}}, + }, + { + name: "ZRANGE with positive indices and scores", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6", "ZRANGE key 0 10 WITHSCORES"}, + expected: []interface{}{int64(6), []interface{}{"member1", "1", "member2", "2", "member3", "3", "member4", "4", "member5", "5", "member6", "6"}}, + }, + { + name: "ZRANGE with positive indices and scores in reverse order", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6", "ZRANGE key 0 10 REV WITHSCORES"}, + expected: []interface{}{int64(6), []interface{}{"member6", "6", "member5", "5", "member4", "4", "member3", "3", "member2", "2", "member1", "1"}}, + }, + { + name: "ZRANGE with negative indices", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6", "ZRANGE key -1 -1"}, + expected: []interface{}{int64(6), []interface{}{"member6"}}, + }, + { + name: "ZRANGE with negative indices and scores", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6", "ZRANGE key -8 -5 WITHSCORES"}, + expected: []interface{}{int64(6), []interface{}{"member1", "1", "member2", "2"}}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + FireCommand(conn, "DEL key") // Resetting the key before each test + + // post cleanup + t.Cleanup(func() { + FireCommand(conn, "DEL key") + }) + + for i, cmd := range tc.commands { + result := FireCommand(conn, cmd) + assert.Equal(t, tc.expected[i], result) + } + }) + } +} diff --git a/integration_tests/commands/websocket/zset_test.go b/integration_tests/commands/websocket/zset_test.go index e57cccdf9..e7720eb0d 100644 --- a/integration_tests/commands/websocket/zset_test.go +++ b/integration_tests/commands/websocket/zset_test.go @@ -147,3 +147,541 @@ func TestZCOUNT(t *testing.T) { }) } } + +func TestZADD(t *testing.T) { + exec := NewWebsocketCommandExecutor() + + testCases := []TestCase{ + { + name: "ZADD with two new members", + commands: []string{"ZADD myzset 1 member1 2 member2"}, + expected: []interface{}{float64(2)}, + }, + { + name: "ZADD with three new members", + commands: []string{"ZADD myzset 3 member3 4 member4 5 member5"}, + expected: []interface{}{float64(3)}, + }, + { + name: "ZADD with existing members", + commands: []string{"ZADD myzset 1 member1 2 member2 3 member3 4 member4 5 member5"}, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD with mixed new and existing members", + commands: []string{"ZADD myzset 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6"}, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD without any members", + commands: []string{"ZADD myzset 1"}, + expected: []interface{}{"ERR wrong number of arguments for 'zadd' command"}, + }, + // *************************************** ZADD with XX options validation starts now, including XX with GT, LT, NX, INCR, CH ************************** + { + name: "ZADD XX option without existing key", + commands: []string{"ZADD myzset XX 10 member9"}, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD XX with existing key and member2", + commands: []string{"ZADD myzset XX 20 member2"}, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD XX updates existing elements scores", + commands: []string{"ZADD myzset XX 15 member1 25 member2"}, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD GT and XX only updates existing elements when new scores are greater", + commands: []string{"ZADD myzset GT XX 20 member1"}, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD LT and XX only updates existing elements when new scores are lower", + commands: []string{"ZADD myzset LT XX 20 member1"}, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD NX and XX not compatible", + commands: []string{"ZADD myzset NX XX 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD XX and CH compatible", + commands: []string{"ZADD myzset XX CH 20 member1"}, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD INCR and XX compatible", + commands: []string{"ZADD myzset XX INCR 20 member1"}, + expected: []interface{}{float64(40)}, + }, + { + name: "ZADD INCR and XX not compatible because of more than one member", + commands: []string{"ZADD myzset XX INCR 20 member1 25 member2"}, + expected: []interface{}{"ERR incr option supports a single increment-element pair"}, + }, + + { + name: "ZADD XX, LT and GT are not compatible", + commands: []string{"ZADD key XX LT GT 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD XX, LT, GT, CH, INCR are not compatible", + commands: []string{"ZADD key XX LT GT INCR CH 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + + { + name: "ZADD XX, GT and CH compatible", + commands: []string{"ZADD key XX GT CH 60 member1 30 member2"}, + expected: []interface{}{float64(0)}, + }, + + { + name: "ZADD XX, LT and CH compatible", + commands: []string{"ZADD key XX LT CH 4 member1 1 member2"}, + expected: []interface{}{float64(0)}, + }, + + //running with new members, XX wont update with new members, only existing gets updated + + { + name: "ZADD XX with existing key and new member", + commands: []string{"ZADD key XX 20 member20"}, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD XX wont update as new members", + commands: []string{"ZADD key XX 15 member18 25 member20"}, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD XX and GT wont add new member", + commands: []string{"ZADD key GT XX 20 member18"}, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD XX and LT and new member wont update", + commands: []string{"ZADD key LT XX 20 member18"}, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD XX and CH and new member wont work", + commands: []string{"ZADD key XX CH 20 member18"}, + expected: []interface{}{float64(0)}, + }, + + { + name: "ZADD XX, LT, CH, new member wont update", + commands: []string{"ZADD key XX LT CH 50 member18 40 member20"}, + expected: []interface{}{float64(0)}, + }, + { + name: "ZADD XX, GT and CH, new member wont update", + commands: []string{"ZADD key XX GT CH 60 member18 30 member20"}, + expected: []interface{}{float64(0)}, + }, + + // ******************************************* ZADD with NX starts now, including GT, LT, XX, INCR, CH *************** + + { + name: "ZADD NX existing key new member", + commands: []string{"ZADD key NX 10 member9"}, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD NX existing key old member", + commands: []string{"ZADD key NX 20 member2"}, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD NX existing key one new member and one old member", + commands: []string{"ZADD key NX 15 member1 25 member11"}, + expected: []interface{}{float64(2)}, + }, + + // NX and XX with all LT GT CH and INCR all errors + { + name: "ZADD NX and XX not compatible", + commands: []string{"ZADD key NX XX 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX CH not compatible", + commands: []string{"ZADD key NX XX CH 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX CH INCR not compatible", + commands: []string{"ZADD key NX XX CH INCR 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX LT not compatible", + commands: []string{"ZADD key NX XX LT 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX GT not compatible", + commands: []string{"ZADD key NX XX GT 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX LT CH not compatible", + commands: []string{"ZADD key NX XX LT CH 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX LT CH INCR compatible", + commands: []string{"ZADD key NX XX LT CH INCR 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX GT CH not compatible", + commands: []string{"ZADD key NX XX GT CH 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX GT CH INCR not compatible", + commands: []string{"ZADD key NX XX GT CH INCR 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX INCR not compatible", + commands: []string{"ZADD key NX XX INCR 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX INCR LT not compatible", + commands: []string{"ZADD key NX XX INCR LT 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX INCR GT not compatible", + commands: []string{"ZADD key NX XX INCR GT 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX LT GT not compatible", + commands: []string{"ZADD key NX XX LT GT 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX LT GT CH not compatible", + commands: []string{"ZADD key NX XX LT GT CH 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + { + name: "ZADD NX XX LT GT CH INCR not compatible", + commands: []string{"ZADD key NX XX LT GT CH INCR 20 member1"}, + expected: []interface{}{"ERR xx and nx options at the same time are not compatible"}, + }, + + // NX without XX and all LT GT CH and INCR // all are error + { + name: "ZADD NX and GT incompatible", + commands: []string{"ZADD key NX GT 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX and LT incompatible", + commands: []string{"ZADD key NX LT 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT and GT incompatible", + commands: []string{"ZADD key NX LT GT 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT, GT and INCR incompatible", + commands: []string{"ZADD key NX LT GT INCR 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT, GT and CH incompatible", + commands: []string{"ZADD key NX LT GT CH 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT, GT, CH and INCR incompatible", + commands: []string{"ZADD key NX LT GT CH INCR 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT, CH not compatible", + commands: []string{"ZADD key NX LT CH 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT, INCR not compatible", + commands: []string{"ZADD key NX LT INCR 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, LT, CH, INCR not compatible", + commands: []string{"ZADD key NX LT CH INCR 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, GT, CH not compatible", + commands: []string{"ZADD key NX GT CH 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, GT, INCR not compatible", + commands: []string{"ZADD key NX GT INCR 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD NX, GT, CH, INCR not compatible", + commands: []string{"ZADD key NX GT CH INCR 20 member1"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + + { + name: "ZADD NX, CH with new member returns CH based - if added or not", + commands: []string{"ZADD key NX CH 20 member13"}, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD NX, CH with existing member returns CH based - if added or not", + commands: []string{"ZADD key NX CH 10 member13"}, + expected: []interface{}{float64(1)}, + }, + + // *************************************** ZADD with GT options validation starts now, including GT with XX, LT, NX, INCR, CH ************************** + + { + name: "ZADD with GT with existing member", + commands: []string{"ZADD key GT 15 member14"}, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD with GT with new member", + commands: []string{"ZADD key GT 15 member15"}, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD GT and LT", + commands: []string{"ZADD key GT LT 15 member15"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD GT LT CH", + commands: []string{"ZADD key GT LT CH 15 member15"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD GT LT CH INCR", + commands: []string{"ZADD key GT LT CH INCR 15 member15"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD GT LT INCR", + commands: []string{"ZADD key GT LT INCR 15 member15"}, + expected: []interface{}{"ERR gt and LT and NX options at the same time are not compatible"}, + }, + { + name: "ZADD GT CH with existing member score less no change hence 0", + commands: []string{"ZADD key GT CH 10 member15"}, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD GT CH with existing member score more, changed score hence 1", + commands: []string{"ZADD key GT CH 25 member15"}, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD GT CH with existing member score equal, nothing returned", + commands: []string{"ZADD key GT CH 25 member15"}, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD GT CH with new member score", + commands: []string{"ZADD key GT CH 5 member19"}, + expected: []interface{}{float64(1)}, + }, + { + name: "ZADD GT with INCR if score less than currentscore after INCR returns nil", + commands: []string{"ZADD key GT INCR -5 member15"}, + expected: []interface{}{float64(-5)}, + }, + { + name: "ZADD GT with INCR updates existing member score if greater after INCR", + commands: []string{"ZADD key GT INCR 5 member15"}, + expected: []interface{}{float64(5)}, + }, + + // *************************************** ZADD with LT options validation starts now, including LT with GT, XX, NX, INCR, CH ************************** + + { + name: "ZADD with LT with existing member score greater", + commands: []string{"ZADD key LT 15 member14"}, + expected: []interface{}{float64(1)}, + }, + + { + name: "ZADD with LT with new member", + commands: []string{"ZADD key LT 15 member23"}, + expected: []interface{}{float64(1)}, + }, + + { + name: "ZADD LT with existing member score equal", + commands: []string{"ZADD key LT 15 member14"}, + expected: []interface{}{float64(1)}, + }, + + { + name: "ZADD LT with existing member score less", + commands: []string{"ZADD key LT 10 member14"}, + expected: []interface{}{float64(1)}, + }, + + { + name: "ZADD LT with INCR not updates existing member as score is greater after INCR", + commands: []string{"ZADD key LT INCR 5 member14"}, + expected: []interface{}{float64(5)}, + }, + + { + name: "ZADD LT with INCR updates existing member as updatedscore after INCR is less than current", + commands: []string{"ZADD key LT INCR -1 member14"}, + expected: []interface{}{float64(-1)}, + }, + + { + name: "ZADD LT with CH updates existing member score if less, CH returns changed elements", + commands: []string{"ZADD key LT CH 5 member1 2 member2"}, + expected: []interface{}{float64(2)}, + }, + + // *************************************** ZADD with INCR options validation starts now, including INCR with GT, LT, NX, XX, CH ************************** + { + name: "ZADD INCR with new members, insert as it is ", + commands: []string{"ZADD key INCR 15 member24"}, + expected: []interface{}{float64(15)}, + }, + + { + name: "ZADD INCR with existing members, increase the score", + commands: []string{"ZADD key INCR 5 member24"}, + expected: []interface{}{float64(5)}, + }, + + // *************************************** ZADD with CH options validation starts now, including CH with GT, LT, NX, XX, INCR ************************** + { + name: "ZADD CH with one existing members update, returns count of updation", + commands: []string{"ZADD key CH 45 member2"}, + expected: []interface{}{float64(1)}, + }, + + { + name: "ZADD CH with multiple existing members update, returns count of updation", + commands: []string{"ZADD key CH 50 member2 63 member3"}, + expected: []interface{}{float64(2)}, + }, + + { + name: "ZADD CH with 1 new and 1 existing member update, returns count of updation", + commands: []string{"ZADD key CH 50 member2 64 member32"}, + expected: []interface{}{float64(2)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + conn := exec.ConnectToServer() + + DeleteKey(t, conn, exec, "key") + + //posrcleanup + + t.Cleanup(func() { + DeleteKey(t, conn, exec, "key") + }) + + for i, cmd := range tc.commands { + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.Nil(t, err) + assert.Equal(t, tc.expected[i], result) + } + }) + } +} + +func TestZRANGE(t *testing.T) { + exec := NewWebsocketCommandExecutor() + testCases := []TestCase{ + { + name: "ZRANGE with mixed indices", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6", "ZRANGE key 0 -1"}, + expected: []interface{}{float64(6), []interface{}{"member1", "member2", "member3", "member4", "member5", "member6"}}, + }, + { + name: "ZRANGE with positive indices #1", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6", "ZRANGE key 0 2"}, + expected: []interface{}{float64(6), []interface{}{"member1", "member2", "member3"}}, + }, + { + name: "ZRANGE with positive indices #2", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6", "ZRANGE key 2 4"}, + expected: []interface{}{float64(6), []interface{}{"member3", "member4", "member5"}}, + }, + { + name: "ZRANGE with all positive indices", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6", "ZRANGE key 0 10"}, + expected: []interface{}{float64(6), []interface{}{"member1", "member2", "member3", "member4", "member5", "member6"}}, + }, + { + name: "ZRANGE with out of bound indices", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6", "ZRANGE key 10 20"}, + expected: []interface{}{float64(6), []interface{}{}}, + }, + { + name: "ZRANGE with positive indices and scores", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6", "ZRANGE key 0 10 WITHSCORES"}, + expected: []interface{}{float64(6), []interface{}{"member1", "1", "member2", "2", "member3", "3", "member4", "4", "member5", "5", "member6", "6"}}, + }, + { + name: "ZRANGE with positive indices and scores in reverse order", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6", "ZRANGE key 0 10 REV WITHSCORES"}, + expected: []interface{}{float64(6), []interface{}{"member6", "6", "member5", "5", "member4", "4", "member3", "3", "member2", "2", "member1", "1"}}, + }, + { + name: "ZRANGE with negative indices", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6", "ZRANGE key -1 -1"}, + expected: []interface{}{float64(6), []interface{}{"member6"}}, + }, + { + name: "ZRANGE with negative indices and scores", + commands: []string{"ZADD key 1 member1 2 member2 3 member3 4 member4 5 member5 6 member6", "ZRANGE key -8 -5 WITHSCORES"}, + expected: []interface{}{float64(6), []interface{}{"member1", "1", "member2", "2"}}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + conn := exec.ConnectToServer() + + DeleteKey(t, conn, exec, "key") + + //posrcleanup + + t.Cleanup(func() { + DeleteKey(t, conn, exec, "key") + }) + + for i, cmd := range tc.commands { + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.Nil(t, err) + assert.Equal(t, tc.expected[i], result) + } + }) + } +} diff --git a/internal/eval/constants.go b/internal/eval/constants.go index 2d7a7cecc..a7d924b6a 100644 --- a/internal/eval/constants.go +++ b/internal/eval/constants.go @@ -18,6 +18,8 @@ const ( NX string = "NX" GT string = "GT" LT string = "LT" + CH string = "CH" + INCR string = "INCR" KeepTTL string = "KEEPTTL" Sync string = "SYNC" Async string = "ASYNC" diff --git a/internal/eval/eval.go b/internal/eval/eval.go index 8bf1fb8c5..0d937fe7e 100644 --- a/internal/eval/eval.go +++ b/internal/eval/eval.go @@ -2438,10 +2438,10 @@ func evalGEOADD(args []string, store *dstore.Store) []byte { // Parse options for startIdx < len(args) { option := strings.ToUpper(args[startIdx]) - if option == "NX" { + if option == NX { nx = true startIdx++ - } else if option == "XX" { + } else if option == XX { xx = true startIdx++ } else { diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go index 1d3ce1bb1..746c5c84a 100644 --- a/internal/eval/store_eval.go +++ b/internal/eval/store_eval.go @@ -695,32 +695,94 @@ func evalGETRANGE(args []string, store *dstore.Store) *EvalResponse { // reinserted at the right position to ensure the correct ordering. // If key does not exist, a new sorted set with the specified members as sole members is created. func evalZADD(args []string, store *dstore.Store) *EvalResponse { - if len(args) < 3 || len(args)%2 == 0 { + // if length of command is 3, throw error as it is not possible + if len(args) < 3 { return &EvalResponse{ Result: nil, Error: diceerrors.ErrWrongArgumentCount("ZADD"), } } - key := args[0] - obj := store.Get(key) - var sortedSet *sortedset.Set + sortedSet, err := getOrCreateSortedSet(store, key) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: err, + } + } + // flags parsing + flags, nextIndex := parseFlags(args[1:]) + if nextIndex >= len(args) || (len(args)-nextIndex)%2 != 0 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("ZADD"), + } + } + // only valid flags works + if err := validateFlagsAndArgs(args[nextIndex:], flags); err != nil { + return &EvalResponse{ + Result: nil, + Error: err, + } + } + // all processing takes place here + return processMembersWithFlags(args[nextIndex:], sortedSet, store, key, flags) +} - if obj != nil { - var err []byte - sortedSet, err = sortedset.FromObject(obj) - if err != nil { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrWrongTypeOperation, - } +// parseFlags identifies and parses the flags used in ZADD. +func parseFlags(args []string) (parsedFlags map[string]bool, nextIndex int) { + parsedFlags = map[string]bool{ + NX: false, + XX: false, + LT: false, + GT: false, + CH: false, + INCR: false, + } + for i := 0; i < len(args); i++ { + switch strings.ToUpper(args[i]) { + case NX: + parsedFlags[NX] = true + case XX: + parsedFlags[XX] = true + case LT: + parsedFlags[LT] = true + case GT: + parsedFlags[GT] = true + case CH: + parsedFlags[CH] = true + case INCR: + parsedFlags[INCR] = true + default: + return parsedFlags, i + 1 } - } else { - sortedSet = sortedset.New() } - added := 0 - for i := 1; i < len(args); i += 2 { + return parsedFlags, len(args) + 1 +} + +// only valid combination of options works +func validateFlagsAndArgs(args []string, flags map[string]bool) error { + if len(args)%2 != 0 { + return diceerrors.ErrGeneral("syntax error") + } + if flags[NX] && flags[XX] { + return diceerrors.ErrGeneral("xx and nx options at the same time are not compatible") + } + if (flags[GT] && flags[NX]) || (flags[LT] && flags[NX]) || (flags[GT] && flags[LT]) { + return diceerrors.ErrGeneral("gt and LT and NX options at the same time are not compatible") + } + if flags[INCR] && len(args)/2 > 1 { + return diceerrors.ErrGeneral("incr option supports a single increment-element pair") + } + return nil +} + +// processMembersWithFlags processes the members and scores while handling flags. +func processMembersWithFlags(args []string, sortedSet *sortedset.Set, store *dstore.Store, key string, flags map[string]bool) *EvalResponse { + added, updated := 0, 0 + + for i := 0; i < len(args); i += 2 { scoreStr := args[i] member := args[i+1] @@ -732,22 +794,93 @@ func evalZADD(args []string, store *dstore.Store) *EvalResponse { } } + currentScore, exists := sortedSet.Get(member) + + // If INCR is used, increment the score first + if flags[INCR] { + if exists { + score += currentScore + } else { + score = 0.0 + score + } + + // Now check GT and LT conditions based on the incremented score and return accordingly + if (flags[GT] && exists && score <= currentScore) || + (flags[LT] && exists && score >= currentScore) { + return &EvalResponse{ + Result: nil, + Error: nil, + } + } + } + + // Check if the member should be skipped based on NX or XX flags + if shouldSkipMember(score, currentScore, exists, flags) { + continue + } + + // Insert or update the member in the sorted set wasInserted := sortedSet.Upsert(score, member) - if wasInserted { - added += 1 + if wasInserted && !exists { + added++ + } else if exists && score != currentScore { + updated++ + } + + // If INCR is used, exit after processing one score-member pair + if flags[INCR] { + return &EvalResponse{ + Result: score, + Error: nil, + } } } - obj = store.NewObj(sortedSet, -1, object.ObjTypeSortedSet, object.ObjEncodingBTree) - store.Put(key, obj, dstore.WithPutCmd(dstore.ZAdd)) + // Store the updated sorted set in the store + storeUpdatedSet(store, key, sortedSet) + + if flags[CH] { + return &EvalResponse{ + Result: added + updated, + Error: nil, + } + } + // Return only the count of added members return &EvalResponse{ Result: added, Error: nil, } } +// shouldSkipMember determines if a member should be skipped based on flags. +func shouldSkipMember(score, currentScore float64, exists bool, flags map[string]bool) bool { + useNX, useXX, useLT, useGT := flags[NX], flags[XX], flags[LT], flags[GT] + + return (useNX && exists) || (useXX && !exists) || + (exists && useLT && score >= currentScore) || + (exists && useGT && score <= currentScore) +} + +// storeUpdatedSet stores the updated sorted set in the store. +func storeUpdatedSet(store *dstore.Store, key string, sortedSet *sortedset.Set) { + store.Put(key, store.NewObj(sortedSet, -1, object.ObjTypeSortedSet, object.ObjEncodingBTree), dstore.WithPutCmd(dstore.ZAdd)) +} + +// getOrCreateSortedSet fetches the sorted set if it exists, otherwise creates a new one. +func getOrCreateSortedSet(store *dstore.Store, key string) (*sortedset.Set, error) { + obj := store.Get(key) + if obj != nil { + sortedSet, err := sortedset.FromObject(obj) + if err != nil { + return nil, diceerrors.ErrWrongTypeOperation + } + return sortedSet, nil + } + return sortedset.New(), nil +} + // The ZCOUNT command in DiceDB counts the number of members in a sorted set at the specified key // whose scores fall within a given range. The command takes three arguments: the key of the sorted set // the minimum score, and the maximum score. diff --git a/internal/store/constants.go b/internal/store/constants.go index 9bd369d93..53deb0050 100644 --- a/internal/store/constants.go +++ b/internal/store/constants.go @@ -1,13 +1,13 @@ package store const ( - Set string = "SET" - Del string = "DEL" - Get string = "GET" - Rename string = "RENAME" - ZAdd string = "ZADD" - ZRange string = "ZRANGE" - PFADD string = "PFADD" + Set string = "SET" + Del string = "DEL" + Get string = "GET" + Rename string = "RENAME" + ZAdd string = "ZADD" + ZRange string = "ZRANGE" + PFADD string = "PFADD" PFCOUNT string = "PFCOUNT" PFMERGE string = "PFMERGE" ) diff --git a/internal/watchmanager/watch_manager.go b/internal/watchmanager/watch_manager.go index 626a1c485..52adefb95 100644 --- a/internal/watchmanager/watch_manager.go +++ b/internal/watchmanager/watch_manager.go @@ -28,12 +28,12 @@ type ( var ( affectedCmdMap = map[string]map[string]struct{}{ - dstore.Set: {dstore.Get: struct{}{}}, - dstore.Del: {dstore.Get: struct{}{}}, - dstore.Rename: {dstore.Get: struct{}{}}, - dstore.ZAdd: {dstore.ZRange: struct{}{}}, - dstore.PFADD: {dstore.PFCOUNT: struct{}{}}, - dstore.PFMERGE:{dstore.PFCOUNT: struct{}{}}, + dstore.Set: {dstore.Get: struct{}{}}, + dstore.Del: {dstore.Get: struct{}{}}, + dstore.Rename: {dstore.Get: struct{}{}}, + dstore.ZAdd: {dstore.ZRange: struct{}{}}, + dstore.PFADD: {dstore.PFCOUNT: struct{}{}}, + dstore.PFMERGE: {dstore.PFCOUNT: struct{}{}}, } )