diff --git a/.github/workflows/full-test-suite.yml b/.github/workflows/full-test-suite.yml index fe486d978..1a01a31dd 100644 --- a/.github/workflows/full-test-suite.yml +++ b/.github/workflows/full-test-suite.yml @@ -41,11 +41,19 @@ jobs: key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }} restore-keys: | ${{ runner.os }}-go- + - uses: dorny/paths-filter@v3 + id: file_types + with: + filters: | + watch_file_changes: + - '**/*.go' - name: Install dependencies run: go get -v . # Check if there is any version changes from the cache - name: Build run: make build - name: Run Unit tests + if: steps.file_types.outputs.watch_file_changes == 'true' run: make unittest - name: Run Integration tests - run: make test \ No newline at end of file + if: steps.file_types.outputs.watch_file_changes == 'true' + run: make test diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 36571ac22..fb7c4a708 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -21,9 +21,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: stable + - uses: actions/cache@v4 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-golint-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-golint- - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: diff --git a/docs/src/content/docs/commands/APPEND.md b/docs/src/content/docs/commands/APPEND.md index 8ada6fd98..2f5cedcf0 100644 --- a/docs/src/content/docs/commands/APPEND.md +++ b/docs/src/content/docs/commands/APPEND.md @@ -3,7 +3,7 @@ title: APPEND description: The `APPEND` command in DiceDB is used to either set the value of a key or append a value to an existing key. This command allows for both creating and updating key-value pairs. --- -The `APPEND` command in DiceDB is used to either set the value of a key or append a value to an existing key. This command allows for both creating and updating key-value pairs. +The `APPEND` command in DiceDB is used to either set the value of a key or append a value to an existing key and returns the length of the value stored at the specified key after appending. This command allows for both creating and updating key-value pairs. ## Syntax @@ -56,8 +56,34 @@ Appending to key `foo` that contains `bar` with `baz` ```bash 127.0.0.1:7379> SET foo bar +OK 127.0.0.1:7379> APPEND foo baz (integer) 6 +127.0.0.1:7379> GET foo +"barbaz" +``` + +Appending "1" to key `bmkey` that contains a bitmap equivalent of `42` + +```bash +127.0.0.1:7379> SETBIT bmkey 2 1 +(integer) 0 +127.0.0.1:7379> SETBIT bmkey 3 1 +(integer) 0 +127.0.0.1:7379> SETBIT bmkey 5 1 +(integer) 0 +127.0.0.1:7379> SETBIT bmkey 10 1 +(integer) 0 +127.0.0.1:7379> SETBIT bmkey 11 1 +(integer) 0 +127.0.0.1:7379> SETBIT bmkey 14 1 +(integer) 0 +127.0.0.1:7379> GET bmkey +"42" +127.0.0.1:7379> APPEND bmkey 1 +(integer) 3 +127.0.0.1:7379> GET bmkey +"421" ``` ### Invalid usage diff --git a/docs/src/content/docs/commands/GEOADD.md b/docs/src/content/docs/commands/GEOADD.md new file mode 100644 index 000000000..19ba0cafe --- /dev/null +++ b/docs/src/content/docs/commands/GEOADD.md @@ -0,0 +1,90 @@ +--- +title: GEOADD +description: The `GEOADD` command in DiceDB is used to add geospatial items (longitude,latitude) to a specified key, storing them as a sorted set. This would allow for efficient querying of geographical data using commands like GEOSEARCH. +--- + +The `GEOADD` command in DiceDB is used to add geospatial items (longitude,latitude) to a specified key, storing them as a sorted set. This would allow for efficient querying of geographical data using commands like GEOSEARCH. + +## Syntax + +```bash +GEOADD key [NX | XX] [CH] longitude latitude member [longitude latitude member ...] +``` + +## Parameters + +| Parameter | Description | Type | Required | +| --------- | --------------------------------------------------------------------------------- | ------ | -------- | +| key | The name of the sorted set where the geospatial data will be stored. | string | Yes | +| NX | Only add new elements; do not update existing ones. | NONE | No | +| XX | Only update existing elements; do not add new ones. | NONE | No | +| longitude | longitude of the location (must be between -180 and 180 degrees). | float | Yes | +| latitude | latitude of the location (must be between -85.05112878 and 85.05112878 degrees). | float | Yes | +| member | A unique identifier for the location. | string | Yes | + + +## Return Values + +| Condition | Return Value | +| ------------------------------------------------------------ | ----------------------------------------------------------- | +| For each new member added. | 1 | +| No new member is added. | 0 | +| Incorrect Argument Count |`ERR wrong number of arguments for 'geoadd' command` | +| If the longitude is not a valid number or is out of range. |`ERR invalid longitude` | +| If the latitude is not a valid number or is out of range. |`ERR invalid latitude` | + +## Behaviour + +When the GEOADD command is issued, DiceDB performs the following steps: + +1. It checks whether longitude and latitude are valid or not. If not an error is thrown. +2. It checks whether the set exists or not. +3. If set doesn't exist new set is created or else the same set is used. +4. It adds or updates the member in the set. +5. It returns number of members added. + +## Errors + +1.`Wrong number of arguments for 'GEOADD' command` + - Error Message: (error) ERR wrong number of arguments for 'geoadd' command. + - Occurs when the command is executed with an incorrect number of arguments. + +2. `Longitude not a valid number or is out of range ` + - Error Message: (error) ERR invalid longitude. + - Occurs when longitude is out of range(-180 to 180) or not a valid number. + +3. `Latitude not a valid number or is out of range ` + - Error Message: (error) ERR invalid latitude. + - Occurs when latitude is out of range(-85.05112878 to 85.05112878) or not a valid number. + +## Example Usage + +Here are a few examples demonstrating the usage of the GEOADD command: + +### Example : Adding new member to a set + +```bash +127.0.0.1:7379> GEOADD locations 13.361389 38.115556 "Palermo" +1 +``` + +### Example : Updating an already existing member to a set + +```bash +127.0.0.1:7379> GEOADD locations 13.361389 39.115556 "Palermo" +0 +``` + +### Example : Error Adding a member with invalid longitude + +```bash +127.0.0.1:7379> GEOADD locations 181.120332 39.115556 "Jamaica" +(error) ERROR invalid longitude +``` + +### Example : Error Adding a member with invalid latitde + +```bash +127.0.0.1:7379> GEOADD locations 13.361389 91.115556 "Venice" +(error) ERROR invalid latitude +``` diff --git a/docs/src/content/docs/commands/GEODIST.md b/docs/src/content/docs/commands/GEODIST.md new file mode 100644 index 000000000..c40c529d2 --- /dev/null +++ b/docs/src/content/docs/commands/GEODIST.md @@ -0,0 +1,71 @@ +--- +title: GEODIST +description: The `GEODIST` command in Redis is used to calculate the distance between two members (geospatial points) stored in a geospatial index(set). +--- + +The `GEODIST` command in DiceDB is used to calculate the distance between two members (geospatial points) stored in a geospatial index(set). + +## Syntax + +```bash +GEODIST key member1 member2 [m | km | ft | mi] +``` + +## Parameters + +| Parameter | Description | Type | Required | +| --------- | --------------------------------------------------------------------------------- | ------ | -------- | +| key | The name of the sorted set where the geospatial data is stored. | string | Yes | +| member1 | The name of the member1 from where you want to measure the distance. | string | Yes | +| member2 | The name of the member2 to where you want to measure the distance. | string | Yes | +| m | The distance to be measured in meters. | NONE | NO | +| km | The distance to be measured in kilometers. | NONE | NO | +| ft | The distance to be measured in feet. | NONE | NO | +| mi | The distance to be measured in miles. | NONE | NO | + + +## Return Values + +| Condition | Return Value | +| ------------------------------------------------------------ | ----------------------------------------------------------- | +| If both members exist in the set with no option | distance b/w them in meters | +| If both members exist in the set with option km | distance b/w them in kilometers | +| If both members exist in the set with option ft | distance b/w them in feet | +| If both members exist in the set with option mi | distance b/w them in miles | +| If any member doesn't exist in Set | nil | +| Incorrect Argument Count |`ERR wrong number of arguments for 'geodist' command` | + +## Behaviour + +When the GEODIST command is issued, DiceDB performs the following steps: + +1. It gets the sorted set(key). +2. It gets the scores(geohashes) from the sorted sets for both the members. +3. It calculates the distance bw them and returns it. + +## Errors + +1.`Wrong number of arguments for 'GEODIST' command` + - Error Message: (error) ERR wrong number of arguments for 'geodist' command. + - Occurs when the command is executed with an incorrect number of arguments. + + +## Example Usage + +Here are a few examples demonstrating the usage of the GEODIST command: + +### Example : Adding new member to a set + +```bash +127.0.0.1:7379> GEOADD cities -74.0060 40.7128 "New York" +1 +127.0.0.1:7379> GEOADD cities -79.3470 43.6510 "Toronto" +1 +127.0.0.1:7379> GEODIST cities "New York" "Toronto" +"548064.1868" +127.0.0.1:7379> GEODIST cities "New York" "Toronto km" +"548.0642" +127.0.0.1:7379> GEODIST cities "New York" "Toronto mi" +"340.5521" +``` + diff --git a/docs/src/content/docs/commands/ZADD.md b/docs/src/content/docs/commands/ZADD.md new file mode 100644 index 000000000..2b684452b --- /dev/null +++ b/docs/src/content/docs/commands/ZADD.md @@ -0,0 +1,123 @@ +--- +title: ZADD +description: The ZADD command adds one or more members with scores to a sorted set in DiceDB. If the key doesn't exist, it creates a new sorted set. If a member already exists, its score is updated. This command is essential for managing sorted data efficiently. +--- + +The ZADD command in DiceDB is used to add one or more members with their associated scores to a sorted set. If the specified key doesn't exist, it creates a new sorted set. For existing members, their scores are updated. This command is crucial for maintaining ordered data structures efficiently. + +## Syntax + +```bash +ZADD key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...] +``` + +## Parameters + +| Parameter | Description | Type | Required | +| --------- | ----------------------------------------------------------------------------------------------------------------- | ------ | -------- | +| `key` | The key of the sorted set | String | Yes | +| `score` | The score associated with the member | Float | Yes | +| `member` | The member to be added to the sorted set | String | Yes | +| `NX` | Only add new elements. Don't update existing elements. | Flag | No | +| `XX` | Only update existing elements. Don't add new elements. | Flag | No | +| `GT` | Only update existing elements if the new score is greater than the current score | Flag | No | +| `LT` | Only update existing elements if the new score is less than the current score | Flag | No | +| `CH` | Modify the return value from the number of new elements added, to the total number of elements changed | Flag | No | +| `INCR` | When this option is specified, ZADD acts like ZINCRBY. Only one score-element pair can be specified in this mode. | Flag | No | + +## Return values + +| Condition | Return Value | +| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | +| Command executed successfully | The number of elements added to the sorted set (not including elements already existing for which the score was updated) | +| Key holds a value that is not a sorted set | Error message | + +## Behaviour + +- If the key does not exist, a new sorted set is created and the specified members are added with their respective scores. +- If a specified member already exists in the sorted set, its score is updated to the new score provided. +- Members are always added in sorted order according to their score, from the lowest to the highest. +- If multiple score-member pairs are specified, they are processed left to right. +- The `NX` and `XX` options are mutually exclusive and cannot be used together. +- When `CH` is specified, the command returns the total number of elements changed (added and updated). +- The `INCR` option allows the command to behave like ZINCRBY, incrementing the existing score of a member (or setting it if it doesn't exist). + +## Errors + +1. `Wrong type of value or key`: + + - Error Message: `(error) WRONGTYPE Operation against a key holding the wrong kind of value` + - Occurs when attempting to use ZADD on a key that contains a non-sorted set value. + +2. `Invalid score`: + - Error Message: `(error) ERR value is not an integer or a float` + - Occurs when the provided score is not a valid floating-point number. + +## Example Usage + +### Basic Usage + +```bash +127.0.0.1:7379> ZADD myzset 1 "one" 2 "two" 3 "three" +(integer) 3 +127.0.0.1:7379> ZADD myzset 4 "four" +(integer) 1 +``` + +### Updating Existing Members + +```bash +127.0.0.1:7379> ZADD myzset 5 "two" +(integer) 0 +``` + +### Using NX Option + +```bash +127.0.0.1:7379> ZADD myzset NX 6 "six" 7 "two" +(integer) 1 +``` + +### Using XX Option + +```bash +127.0.0.1:7379> ZADD myzset XX 8 "eight" 9 "two" +(integer) 0 +``` + +### Using CH Option + +```bash +127.0.0.1:7379> ZADD myzset CH 10 "ten" 11 "two" +(integer) 2 +``` + +### using INCR Option + +```bash +127.0.0.1:7379> ZADD myzset INCR 1 "two" +(integer) 12 +``` + +## Invalid Usage + +```bash +127.0.0.1:7379> ZADD myzset NX XX 12 "twelve" +(error) ERR XX and NX options at the same time are not compatible +``` + +```bash +127.0.0.1:7379> ZADD myzset LT GT 15 "twelve" +(error) ERR GT, LT, and/or NX options at the same time are not compatible +``` + +## Best Practices + +- Use appropriate score values to maintain the desired order of elements in the sorted set. +- Consider using the `NX` or `XX` options when you want to specifically add new elements or update existing ones, respectively. +- Use the `CH` option when you need to know the total number of elements changed, including both additions and updates. + +## Notes + +- The time complexity of ZADD is O(log(N)) for each item added, where N is the number of elements in the sorted set. +- Scores can be any double-precision floating-point number. diff --git a/integration_tests/commands/http/append_test.go b/integration_tests/commands/http/append_test.go index 336a37850..3e63f9c1e 100644 --- a/integration_tests/commands/http/append_test.go +++ b/integration_tests/commands/http/append_test.go @@ -101,6 +101,23 @@ func TestAPPEND(t *testing.T) { {Command: "del", Body: map[string]interface{}{"key": "myzset"}}, }, }, + { + name: "APPEND to key created using SETBIT", + commands: []HTTPCommand{ + {Command: "SETBIT", Body: map[string]interface{}{"key": "bitkey", "values": []string{"2", "1"}}}, + {Command: "SETBIT", Body: map[string]interface{}{"key": "bitkey", "values": []string{"3", "1"}}}, + {Command: "SETBIT", Body: map[string]interface{}{"key": "bitkey", "values": []string{"5", "1"}}}, + {Command: "SETBIT", Body: map[string]interface{}{"key": "bitkey", "values": []string{"10", "1"}}}, + {Command: "SETBIT", Body: map[string]interface{}{"key": "bitkey", "values": []string{"11", "1"}}}, + {Command: "SETBIT", Body: map[string]interface{}{"key": "bitkey", "values": []string{"14", "1"}}}, + {Command: "APPEND", Body: map[string]interface{}{"key": "bitkey", "value": "1"}}, + {Command: "GET", Body: map[string]interface{}{"key": "bitkey"}}, + }, + expected: []interface{}{float64(0), float64(0), float64(0), float64(0), float64(0), float64(0), float64(3), "421"}, + cleanup: []HTTPCommand{ + {Command: "del", Body: map[string]interface{}{"key": "bitkey"}}, + }, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/integration_tests/commands/http/geo_test.go b/integration_tests/commands/http/geo_test.go new file mode 100644 index 000000000..a8e5c6e93 --- /dev/null +++ b/integration_tests/commands/http/geo_test.go @@ -0,0 +1,86 @@ +package http + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGeoAdd(t *testing.T) { + exec := NewHTTPCommandExecutor() + + testCases := []struct { + name string + commands []HTTPCommand + expected []interface{} + }{ + { + name: "GEOADD with wrong number of arguments", + commands: []HTTPCommand{ + {Command: "GEOADD", Body: map[string]interface{}{"key": "mygeo", "values": []interface{}{"1.2", "2.4"}}}, + }, + expected: []interface{}{"ERR wrong number of arguments for 'geoadd' command"}, + }, + { + name: "GEOADD Commands with new member and updating it", + commands: []HTTPCommand{ + {Command: "GEOADD", Body: map[string]interface{}{"key": "mygeo", "values": []interface{}{"1.2", "2.4", "NJ"}}}, + {Command: "GEOADD", Body: map[string]interface{}{"key": "mygeo", "values": []interface{}{"1.24", "2.48", "NJ"}}}, + }, + expected: []interface{}{float64(1), float64(0)}, + }, + { + name: "GEOADD Adding both XX and NX options together", + commands: []HTTPCommand{ + {Command: "GEOADD", Body: map[string]interface{}{"key": "mygeo", "values": []interface{}{"XX", "NX", "1.2", "2.4", "NJ"}}}, + }, + expected: []interface{}{"ERR XX and NX options at the same time are not compatible"}, + }, + { + name: "GEOADD Invalid Longitude", + commands: []HTTPCommand{ + {Command: "GEOADD", Body: map[string]interface{}{"key": "mygeo", "values": []interface{}{"181", "2.4", "MT"}}}, + }, + expected: []interface{}{"ERR invalid longitude"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, cmd := range tc.commands { + result, _ := exec.FireCommand(cmd) + assert.Equal(t, tc.expected[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } +} + +func TestGeoDist(t *testing.T) { + exec := NewHTTPCommandExecutor() + + testCases := []struct { + name string + commands []HTTPCommand + expected []interface{} + }{ + { + name: "GEODIST b/w existing points", + commands: []HTTPCommand{ + {Command: "GEOADD", Body: map[string]interface{}{"key": "points", "values": []interface{}{"13.361389", "38.115556", "Palermo"}}}, + {Command: "GEOADD", Body: map[string]interface{}{"key": "points", "values": []interface{}{"15.087269", "37.502669", "Catania"}}}, + {Command: "GEODIST", Body: map[string]interface{}{"key": "points", "values": []interface{}{"Palermo", "Catania"}}}, + {Command: "GEODIST", Body: map[string]interface{}{"key": "points", "values": []interface{}{"Palermo", "Catania", "km"}}}, + }, + expected: []interface{}{float64(1), float64(1), float64(166274.144), float64(166.2741)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, cmd := range tc.commands { + result, _ := exec.FireCommand(cmd) + assert.Equal(t, tc.expected[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } +} diff --git a/integration_tests/commands/http/zset_test.go b/integration_tests/commands/http/zset_test.go index 67e23ce01..1b54678e9 100644 --- a/integration_tests/commands/http/zset_test.go +++ b/integration_tests/commands/http/zset_test.go @@ -284,7 +284,7 @@ func TestZADD(t *testing.T) { 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"}, + expected: []interface{}{"ERR XX and NX options at the same time are not compatible"}, }, { name: "ZADD XX and CH compatible", @@ -305,21 +305,21 @@ func TestZADD(t *testing.T) { 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"}, + 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or NX options at the same time are not compatible"}, }, { name: "ZADD XX, GT and CH compatible", @@ -411,105 +411,105 @@ func TestZADD(t *testing.T) { 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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 @@ -518,84 +518,84 @@ func TestZADD(t *testing.T) { 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or NX options at the same time are not compatible"}, }, { name: "ZADD NX, CH with new member returns CH based - if added or not", @@ -631,28 +631,28 @@ func TestZADD(t *testing.T) { 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or NX options at the same time are not compatible"}, }, { name: "ZADD GT CH with existing member score less no change hence 0", diff --git a/integration_tests/commands/resp/append_test.go b/integration_tests/commands/resp/append_test.go index 6d2481c7d..2d6abb2e4 100644 --- a/integration_tests/commands/resp/append_test.go +++ b/integration_tests/commands/resp/append_test.go @@ -65,6 +65,12 @@ func TestAPPEND(t *testing.T) { expected: []interface{}{int64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"}, cleanup: []string{"del key"}, }, + { + name: "APPEND to key created using SETBIT", + commands: []string{"SETBIT bitkey 2 1", "SETBIT bitkey 3 1", "SETBIT bitkey 5 1", "SETBIT bitkey 10 1", "SETBIT bitkey 11 1", "SETBIT bitkey 14 1", "APPEND bitkey 1", "GET bitkey"}, + expected: []interface{}{int64(0), int64(0), int64(0), int64(0), int64(0), int64(0), int64(3), "421"}, + cleanup: []string{"del bitkey"}, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/integration_tests/commands/resp/geo_test.go b/integration_tests/commands/resp/geo_test.go new file mode 100644 index 000000000..57bd2656a --- /dev/null +++ b/integration_tests/commands/resp/geo_test.go @@ -0,0 +1,86 @@ +package resp + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestGeoAdd(t *testing.T) { + conn := getLocalConnection() + defer conn.Close() + + testCases := []struct { + name string + cmds []string + expect []interface{} + }{ + { + name: "GeoAdd With Wrong Number of Arguments", + cmds: []string{"GEOADD mygeo 1 2"}, + expect: []interface{}{"ERR wrong number of arguments for 'geoadd' command"}, + }, + { + name: "GeoAdd With Adding New Member And Updating it", + cmds: []string{"GEOADD mygeo 1.21 1.44 NJ", "GEOADD mygeo 1.22 1.54 NJ"}, + expect: []interface{}{int64(1), int64(0)}, + }, + { + name: "GeoAdd With Adding New Member And Updating it with NX", + cmds: []string{"GEOADD mygeo 1.21 1.44 MD", "GEOADD mygeo 1.22 1.54 MD"}, + expect: []interface{}{int64(1), int64(0)}, + }, + { + name: "GEOADD with both NX and XX options", + cmds: []string{"GEOADD mygeo NX XX 1.21 1.44 MD"}, + expect: []interface{}{"ERR XX and NX options at the same time are not compatible"}, + }, + { + name: "GEOADD invalid longitude", + cmds: []string{"GEOADD mygeo 181.0 1.44 MD"}, + expect: []interface{}{"ERR invalid longitude"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, cmd := range tc.cmds { + result := FireCommand(conn, cmd) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } +} + +func TestGeoDist(t *testing.T) { + conn := getLocalConnection() + defer conn.Close() + + testCases := []struct { + name string + cmds []string + expect []interface{} + delays []time.Duration + }{ + { + name: "GEODIST b/w existing points", + cmds: []string{ + "GEOADD points 13.361389 38.115556 Palermo", + "GEOADD points 15.087269 37.502669 Catania", + "GEODIST points Palermo Catania", + "GEODIST points Palermo Catania km", + }, + expect: []interface{}{int64(1), int64(1), "166274.144", "166.2741"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, cmd := range tc.cmds { + result := FireCommand(conn, cmd) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } +} diff --git a/integration_tests/commands/resp/zset_test.go b/integration_tests/commands/resp/zset_test.go index 9f246d9ed..83217f593 100644 --- a/integration_tests/commands/resp/zset_test.go +++ b/integration_tests/commands/resp/zset_test.go @@ -188,7 +188,7 @@ func TestZADD(t *testing.T) { { 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"}, + expected: []interface{}{"ERR XX and NX options at the same time are not compatible"}, }, { name: "ZADD XX and CH compatible", @@ -203,18 +203,18 @@ func TestZADD(t *testing.T) { { 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"}, + 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or NX options at the same time are not compatible"}, }, { @@ -290,139 +290,139 @@ func TestZADD(t *testing.T) { { 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or NX options at the same time are not compatible"}, }, { @@ -451,22 +451,22 @@ func TestZADD(t *testing.T) { { 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or NX options at the same time are not compatible"}, }, { name: "ZADD GT CH with existing member score less no change hence 0", diff --git a/integration_tests/commands/websocket/append_test.go b/integration_tests/commands/websocket/append_test.go index 989463d96..63ec1b5f9 100644 --- a/integration_tests/commands/websocket/append_test.go +++ b/integration_tests/commands/websocket/append_test.go @@ -51,6 +51,12 @@ func TestAppend(t *testing.T) { expected: []interface{}{float64(1), "WRONGTYPE Operation against a key holding the wrong kind of value"}, cleanupKey: "key", }, + { + name: "APPEND to key created using SETBIT", + commands: []string{"SETBIT bitkey 2 1", "SETBIT bitkey 3 1", "SETBIT bitkey 5 1", "SETBIT bitkey 10 1", "SETBIT bitkey 11 1", "SETBIT bitkey 14 1", "APPEND bitkey 1", "GET bitkey"}, + expected: []interface{}{float64(0), float64(0), float64(0), float64(0), float64(0), float64(0), float64(3), "421"}, + cleanupKey: "bitkey", + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/integration_tests/commands/websocket/geo_test.go b/integration_tests/commands/websocket/geo_test.go new file mode 100644 index 000000000..e2c5d240b --- /dev/null +++ b/integration_tests/commands/websocket/geo_test.go @@ -0,0 +1,87 @@ +package websocket + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGeoAdd(t *testing.T) { + exec := NewWebsocketCommandExecutor() + conn := exec.ConnectToServer() + + testCases := []struct { + name string + cmds []string + expect []interface{} + }{ + { + name: "GeoAdd With Wrong Number of Arguments", + cmds: []string{"GEOADD mygeo 1 2"}, + expect: []interface{}{"ERR wrong number of arguments for 'geoadd' command"}, + }, + { + name: "GeoAdd With Adding New Member And Updating it", + cmds: []string{"GEOADD mygeo 1.21 1.44 NJ", "GEOADD mygeo 1.22 1.54 NJ"}, + expect: []interface{}{float64(1), float64(0)}, + }, + { + name: "GeoAdd With Adding New Member And Updating it with NX", + cmds: []string{"GEOADD mygeo NX 1.21 1.44 MD", "GEOADD mygeo 1.22 1.54 MD"}, + expect: []interface{}{float64(1), float64(0)}, + }, + { + name: "GEOADD with both NX and XX options", + cmds: []string{"GEOADD mygeo NX XX 1.21 1.44 DEL"}, + expect: []interface{}{"ERR XX and NX options at the same time are not compatible"}, + }, + { + name: "GEOADD invalid longitude", + cmds: []string{"GEOADD mygeo 181.0 1.44 MD"}, + expect: []interface{}{"ERR invalid longitude"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, cmd := range tc.cmds { + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.Nil(t, err) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } +} + +func TestGeoDist(t *testing.T) { + exec := NewWebsocketCommandExecutor() + conn := exec.ConnectToServer() + defer conn.Close() + + testCases := []struct { + name string + cmds []string + expect []interface{} + }{ + { + name: "GEODIST b/w existing points", + cmds: []string{ + "GEOADD points 13.361389 38.115556 Palermo", + "GEOADD points 15.087269 37.502669 Catania", + "GEODIST points Palermo Catania", + "GEODIST points Palermo Catania km", + }, + expect: []interface{}{float64(1), float64(1), float64(166274.144), float64(166.2741)}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, cmd := range tc.cmds { + result, err := exec.FireCommandAndReadResponse(conn, cmd) + assert.Nil(t, err) + assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd) + } + }) + } +} diff --git a/integration_tests/commands/websocket/zset_test.go b/integration_tests/commands/websocket/zset_test.go index e7720eb0d..30c4bfd56 100644 --- a/integration_tests/commands/websocket/zset_test.go +++ b/integration_tests/commands/websocket/zset_test.go @@ -206,7 +206,7 @@ func TestZADD(t *testing.T) { { 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"}, + expected: []interface{}{"ERR XX and NX options at the same time are not compatible"}, }, { name: "ZADD XX and CH compatible", @@ -221,18 +221,18 @@ func TestZADD(t *testing.T) { { 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"}, + 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or NX options at the same time are not compatible"}, }, { @@ -308,139 +308,139 @@ func TestZADD(t *testing.T) { { 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or NX options at the same time are not compatible"}, }, { @@ -469,22 +469,22 @@ func TestZADD(t *testing.T) { { 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or 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"}, + expected: []interface{}{"ERR GT, LT, and/or NX options at the same time are not compatible"}, }, { name: "ZADD GT CH with existing member score less no change hence 0", diff --git a/internal/eval/commands.go b/internal/eval/commands.go index 5c8b1ea51..ae901640f 100644 --- a/internal/eval/commands.go +++ b/internal/eval/commands.go @@ -1275,18 +1275,20 @@ var ( NewEval: evalHINCRBYFLOAT, } geoAddCmdMeta = DiceCmdMeta{ - Name: "GEOADD", - Info: `Adds one or more members to a geospatial index. The key is created if it doesn't exist.`, - Arity: -5, - Eval: evalGEOADD, - KeySpecs: KeySpecs{BeginIndex: 1}, + Name: "GEOADD", + Info: `Adds one or more members to a geospatial index. The key is created if it doesn't exist.`, + Arity: -5, + IsMigrated: true, + NewEval: evalGEOADD, + KeySpecs: KeySpecs{BeginIndex: 1}, } geoDistCmdMeta = DiceCmdMeta{ - Name: "GEODIST", - Info: `Returns the distance between two members in the geospatial index.`, - Arity: -4, - Eval: evalGEODIST, - KeySpecs: KeySpecs{BeginIndex: 1}, + Name: "GEODIST", + Info: `Returns the distance between two members in the geospatial index.`, + Arity: -4, + IsMigrated: true, + NewEval: evalGEODIST, + KeySpecs: KeySpecs{BeginIndex: 1}, } jsonstrappendCmdMeta = DiceCmdMeta{ Name: "JSON.STRAPPEND", diff --git a/internal/eval/eval.go b/internal/eval/eval.go index 11705958b..dea3856a7 100644 --- a/internal/eval/eval.go +++ b/internal/eval/eval.go @@ -5,14 +5,11 @@ import ( "errors" "fmt" "log/slog" - "math" "sort" "strconv" "strings" "time" - "github.com/dicedb/dice/internal/eval/geo" - "github.com/dicedb/dice/internal/eval/sortedset" "github.com/dicedb/dice/internal/object" "github.com/dicedb/dice/internal/sql" @@ -1363,133 +1360,3 @@ func executeBitfieldOps(value *ByteArray, ops []utils.BitFieldOp) []interface{} } return result } -func evalGEOADD(args []string, store *dstore.Store) []byte { - if len(args) < 4 { - return diceerrors.NewErrArity("GEOADD") - } - - key := args[0] - var nx, xx bool - startIdx := 1 - - // Parse options - for startIdx < len(args) { - option := strings.ToUpper(args[startIdx]) - if option == NX { - nx = true - startIdx++ - } else if option == XX { - xx = true - startIdx++ - } else { - break - } - } - - // Check if we have the correct number of arguments after parsing options - if (len(args)-startIdx)%3 != 0 { - return diceerrors.NewErrArity("GEOADD") - } - - if xx && nx { - return diceerrors.NewErrWithMessage("ERR XX and NX options at the same time are not compatible") - } - - // Get or create sorted set - obj := store.Get(key) - var ss *sortedset.Set - if obj != nil { - var err []byte - ss, err = sortedset.FromObject(obj) - if err != nil { - return err - } - } else { - ss = sortedset.New() - } - - added := 0 - for i := startIdx; i < len(args); i += 3 { - longitude, err := strconv.ParseFloat(args[i], 64) - if err != nil || math.IsNaN(longitude) || longitude < -180 || longitude > 180 { - return diceerrors.NewErrWithMessage("ERR invalid longitude") - } - - latitude, err := strconv.ParseFloat(args[i+1], 64) - if err != nil || math.IsNaN(latitude) || latitude < -85.05112878 || latitude > 85.05112878 { - return diceerrors.NewErrWithMessage("ERR invalid latitude") - } - - member := args[i+2] - _, exists := ss.Get(member) - - // Handle XX option: Only update existing elements - if xx && !exists { - continue - } - - // Handle NX option: Only add new elements - if nx && exists { - continue - } - - hash := geo.EncodeHash(latitude, longitude) - - wasInserted := ss.Upsert(hash, member) - if wasInserted { - added++ - } - } - - obj = store.NewObj(ss, -1, object.ObjTypeSortedSet, object.ObjEncodingBTree) - store.Put(key, obj) - - return clientio.Encode(added, false) -} - -func evalGEODIST(args []string, store *dstore.Store) []byte { - if len(args) < 3 || len(args) > 4 { - return diceerrors.NewErrArity("GEODIST") - } - - key := args[0] - member1 := args[1] - member2 := args[2] - unit := "m" - if len(args) == 4 { - unit = strings.ToLower(args[3]) - } - - // Get the sorted set - obj := store.Get(key) - if obj == nil { - return clientio.RespNIL - } - - ss, err := sortedset.FromObject(obj) - if err != nil { - return err - } - - // Get the scores (geohashes) for both members - score1, ok := ss.Get(member1) - if !ok { - return clientio.RespNIL - } - score2, ok := ss.Get(member2) - if !ok { - return clientio.RespNIL - } - - lat1, lon1 := geo.DecodeHash(score1) - lat2, lon2 := geo.DecodeHash(score2) - - distance := geo.GetDistance(lon1, lat1, lon2, lat2) - - result, err := geo.ConvertDistance(distance, unit) - if err != nil { - return err - } - - return clientio.Encode(utils.RoundToDecimals(result, 4), false) -} diff --git a/internal/eval/eval_test.go b/internal/eval/eval_test.go index e301ce601..bf5dfa76c 100644 --- a/internal/eval/eval_test.go +++ b/internal/eval/eval_test.go @@ -7031,17 +7031,22 @@ func testEvalAPPEND(t *testing.T, store *dstore.Store) { input: []string{"hashKey", "val"}, migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrWrongTypeOperation}, }, - "append to key created using SETBIT": { + "append to key containing byte array": { setup: func() { key := "bitKey" // Create a new byte array object - initialByteArray := NewByteArray(1) // Initialize with 1 byte - initialByteArray.SetBit(0, true) // Set the first bit to 1 + initialByteArray := NewByteArray(2) // Initialize with 2 byte + initialByteArray.SetBit(2, true) // Set the third bit to 1 + initialByteArray.SetBit(3, true) // Set the fourth bit to 1 + initialByteArray.SetBit(5, true) // Set the sixth bit to 1 + initialByteArray.SetBit(10, true) // Set the eleventh bit to 1 + initialByteArray.SetBit(11, true) // Set the twelfth bit to 1 + initialByteArray.SetBit(14, true) // Set the fifteenth bit to 1 obj := store.NewObj(initialByteArray, -1, object.ObjTypeByteArray, object.ObjEncodingByteArray) store.Put(key, obj) }, - input: []string{"bitKey", "val"}, - migratedOutput: EvalResponse{Result: nil, Error: diceerrors.ErrWrongTypeOperation}, + input: []string{"bitKey", "1"}, + migratedOutput: EvalResponse{Result: 3, Error: nil}, }, "append value with leading zeros": { setup: func() { @@ -8372,84 +8377,130 @@ func testEvalBitFieldRO(t *testing.T, store *dstore.Store) { func testEvalGEOADD(t *testing.T, store *dstore.Store) { tests := map[string]evalTestCase{ "GEOADD with wrong number of arguments": { - input: []string{"mygeo", "1", "2"}, - output: diceerrors.NewErrArity("GEOADD"), + input: []string{"mygeo", "1", "2"}, + migratedOutput: EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("GEOADD"), + }, }, "GEOADD with non-numeric longitude": { - input: []string{"mygeo", "long", "40.7128", "NewYork"}, - output: diceerrors.NewErrWithMessage("ERR invalid longitude"), + input: []string{"mygeo", "long", "40.7128", "NewYork"}, + migratedOutput: EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral("invalid longitude"), + }, }, "GEOADD with non-numeric latitude": { - input: []string{"mygeo", "-74.0060", "lat", "NewYork"}, - output: diceerrors.NewErrWithMessage("ERR invalid latitude"), + input: []string{"mygeo", "-74.0060", "lat", "NewYork"}, + migratedOutput: EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral("invalid latitude"), + }, }, "GEOADD new member to non-existing key": { - setup: func() {}, - input: []string{"mygeo", "-74.0060", "40.7128", "NewYork"}, - output: clientio.Encode(int64(1), false), + setup: func() {}, + input: []string{"mygeo", "-74.0060", "40.7128", "NewYork"}, + migratedOutput: EvalResponse{ + Result: 1, + Error: nil, + }, }, "GEOADD existing member with updated coordinates": { setup: func() { evalGEOADD([]string{"mygeo", "-74.0060", "40.7128", "NewYork"}, store) }, - input: []string{"mygeo", "-73.9352", "40.7304", "NewYork"}, - output: clientio.Encode(int64(0), false), + input: []string{"mygeo", "-73.9352", "40.7304", "NewYork"}, + migratedOutput: EvalResponse{ + Result: 0, + Error: nil, + }, }, "GEOADD multiple members": { setup: func() { evalGEOADD([]string{"mygeo", "-74.0060", "40.7128", "NewYork"}, store) }, - input: []string{"mygeo", "-118.2437", "34.0522", "LosAngeles", "-87.6298", "41.8781", "Chicago"}, - output: clientio.Encode(int64(2), false), + input: []string{"mygeo", "-118.2437", "34.0522", "LosAngeles", "-87.6298", "41.8781", "Chicago"}, + migratedOutput: EvalResponse{ + Result: 2, + Error: nil, + }, }, "GEOADD with NX option (new member)": { - input: []string{"mygeo", "NX", "-122.4194", "37.7749", "SanFrancisco"}, - output: clientio.Encode(int64(1), false), + input: []string{"mygeo", "NX", "-122.4194", "37.7749", "SanFrancisco"}, + migratedOutput: EvalResponse{ + Result: 1, + Error: nil, + }, }, "GEOADD with NX option (existing member)": { setup: func() { evalGEOADD([]string{"mygeo", "-74.0060", "40.7128", "NewYork"}, store) }, - input: []string{"mygeo", "NX", "-73.9352", "40.7304", "NewYork"}, - output: clientio.Encode(int64(0), false), + input: []string{"mygeo", "NX", "-73.9352", "40.7304", "NewYork"}, + migratedOutput: EvalResponse{ + Result: 0, + Error: nil, + }, }, "GEOADD with XX option (new member)": { - input: []string{"mygeo", "XX", "-71.0589", "42.3601", "Boston"}, - output: clientio.Encode(int64(0), false), + input: []string{"mygeo", "XX", "-71.0589", "42.3601", "Boston"}, + migratedOutput: EvalResponse{ + Result: 0, + Error: nil, + }, }, "GEOADD with XX option (existing member)": { setup: func() { evalGEOADD([]string{"mygeo", "-74.0060", "40.7128", "NewYork"}, store) }, - input: []string{"mygeo", "XX", "-73.9352", "40.7304", "NewYork"}, - output: clientio.Encode(int64(0), false), + input: []string{"mygeo", "XX", "-73.9352", "40.7304", "NewYork"}, + migratedOutput: EvalResponse{ + Result: 0, + Error: nil, + }, }, "GEOADD with both NX and XX options": { input: []string{"mygeo", "NX", "XX", "-74.0060", "40.7128", "NewYork"}, output: diceerrors.NewErrWithMessage("ERR XX and NX options at the same time are not compatible"), + migratedOutput: EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral("XX and NX options at the same time are not compatible"), + }, }, "GEOADD with invalid option": { - input: []string{"mygeo", "INVALID", "-74.0060", "40.7128", "NewYork"}, - output: diceerrors.NewErrArity("GEOADD"), + input: []string{"mygeo", "INVALID", "-74.0060", "40.7128", "NewYork"}, + migratedOutput: EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("GEOADD"), + }, }, "GEOADD to a key of wrong type": { setup: func() { store.Put("mygeo", store.NewObj("string_value", -1, object.ObjTypeString, object.ObjEncodingRaw)) }, - input: []string{"mygeo", "-74.0060", "40.7128", "NewYork"}, - output: []byte("-ERR Existing key has wrong Dice type\r\n"), + input: []string{"mygeo", "-74.0060", "40.7128", "NewYork"}, + migratedOutput: EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + }, }, "GEOADD with longitude out of range": { - input: []string{"mygeo", "181.0", "40.7128", "Invalid"}, - output: diceerrors.NewErrWithMessage("ERR invalid longitude"), + input: []string{"mygeo", "181.0", "40.7128", "Invalid"}, + migratedOutput: EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral("invalid longitude"), + }, }, "GEOADD with latitude out of range": { - input: []string{"mygeo", "-74.0060", "91.0", "Invalid"}, - output: diceerrors.NewErrWithMessage("ERR invalid latitude"), + input: []string{"mygeo", "-74.0060", "91.0", "Invalid"}, + migratedOutput: EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral("invalid latitude"), + }, }, } - runEvalTests(t, tests, evalGEOADD, store) + runMigratedEvalTests(t, tests, evalGEOADD, store) } func testEvalGEODIST(t *testing.T, store *dstore.Store) { @@ -8459,28 +8510,37 @@ func testEvalGEODIST(t *testing.T, store *dstore.Store) { evalGEOADD([]string{"points", "13.361389", "38.115556", "Palermo"}, store) evalGEOADD([]string{"points", "15.087269", "37.502669", "Catania"}, store) }, - input: []string{"points", "Palermo", "Catania"}, - output: clientio.Encode(float64(166274.1440), false), // Example value + input: []string{"points", "Palermo", "Catania"}, + migratedOutput: EvalResponse{ + Result: float64(166274.1440), + Error: nil, + }, }, "GEODIST with units (km)": { setup: func() { evalGEOADD([]string{"points", "13.361389", "38.115556", "Palermo"}, store) evalGEOADD([]string{"points", "15.087269", "37.502669", "Catania"}, store) }, - input: []string{"points", "Palermo", "Catania", "km"}, - output: clientio.Encode(float64(166.2741), false), // Example value + input: []string{"points", "Palermo", "Catania", "km"}, + migratedOutput: EvalResponse{ + Result: float64(166.2741), + Error: nil, + }, }, "GEODIST to same point": { setup: func() { evalGEOADD([]string{"points", "13.361389", "38.115556", "Palermo"}, store) }, - input: []string{"points", "Palermo", "Palermo"}, - output: clientio.Encode(float64(0.0000), false), // Expecting distance 0 formatted to 4 decimals + input: []string{"points", "Palermo", "Palermo"}, + migratedOutput: EvalResponse{ + Result: float64(0.0000), + Error: nil, + }, }, // Add other test cases here... } - runEvalTests(t, tests, evalGEODIST, store) + runMigratedEvalTests(t, tests, evalGEODIST, store) } func testEvalSINTER(t *testing.T, store *dstore.Store) { diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go index b316af31a..c01dcec12 100644 --- a/internal/eval/store_eval.go +++ b/internal/eval/store_eval.go @@ -18,6 +18,7 @@ import ( "github.com/dicedb/dice/internal/clientio" "github.com/dicedb/dice/internal/cmd" diceerrors "github.com/dicedb/dice/internal/errors" + "github.com/dicedb/dice/internal/eval/geo" "github.com/dicedb/dice/internal/eval/sortedset" "github.com/dicedb/dice/internal/object" "github.com/dicedb/dice/internal/server/utils" @@ -782,13 +783,13 @@ func validateFlagsAndArgs(args []string, flags map[string]bool) error { return diceerrors.ErrGeneral("syntax error") } if flags[NX] && flags[XX] { - return diceerrors.ErrGeneral("xx and nx options at the same time are not compatible") + 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") + return diceerrors.ErrGeneral("GT, LT, and/or 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 diceerrors.ErrGeneral("INCR option supports a single increment-element pair") } return nil } @@ -1115,25 +1116,33 @@ func evalAPPEND(args []string, store *dstore.Store) *EvalResponse { Error: nil, } } - // Key exists path - if _, ok := obj.Value.(*sortedset.Set); ok { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrWrongTypeOperation, - } - } - _, currentEnc := object.ExtractTypeEncoding(obj) var currentValueStr string - switch currentEnc { + switch currentType, currentEnc := object.ExtractTypeEncoding(obj); currentEnc { case object.ObjEncodingInt: // If the encoding is an integer, convert the current value to a string for concatenation currentValueStr = strconv.FormatInt(obj.Value.(int64), 10) case object.ObjEncodingEmbStr, object.ObjEncodingRaw: - // If the encoding is a string, retrieve the string value for concatenation - currentValueStr = obj.Value.(string) + // If the encoding and type is a string, retrieve the string value for concatenation + if currentType == object.ObjTypeString { + currentValueStr = obj.Value.(string) + } else { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + case object.ObjEncodingByteArray: + if val, ok := obj.Value.(*ByteArray); ok { + currentValueStr = string(val.data) + } else { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } default: - // If the encoding is neither integer nor string, return a "wrong type" error + // If the encoding is neither integer, string, nor byte array, return a "wrong type" error return &EvalResponse{ Result: nil, Error: diceerrors.ErrWrongTypeOperation, @@ -1401,7 +1410,6 @@ func jsonGETHelper(store *dstore.Store, path, key string) *EvalResponse { // If path is root, return the entire JSON if path == defaultRootPath { resultBytes, err := sonic.Marshal(jsonData) - fmt.Println(string(resultBytes)) if err != nil { return &EvalResponse{ Result: nil, @@ -5212,7 +5220,6 @@ func evalSETBIT(args []string, store *dstore.Store) *EvalResponse { // resize as per the offset byteArray = byteArray.IncreaseSize(int(requiredByteArraySize)) } - resp := byteArray.GetBit(int(offset)) byteArray.SetBit(int(offset), value) @@ -6165,3 +6172,176 @@ func evalJSONNUMINCRBY(args []string, store *dstore.Store) *EvalResponse { return makeEvalResult(resultString) } + +func evalGEOADD(args []string, store *dstore.Store) *EvalResponse { + if len(args) < 4 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("GEOADD"), + } + } + + key := args[0] + var nx, xx bool + startIdx := 1 + + // Parse options + for startIdx < len(args) { + option := strings.ToUpper(args[startIdx]) + if option == "NX" { + nx = true + startIdx++ + } else if option == "XX" { + xx = true + startIdx++ + } else { + break + } + } + + // Check if we have the correct number of arguments after parsing options + if (len(args)-startIdx)%3 != 0 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("GEOADD"), + } + } + + if xx && nx { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral("XX and NX options at the same time are not compatible"), + } + } + + // Get or create sorted set + obj := store.Get(key) + var ss *sortedset.Set + if obj != nil { + var err []byte + ss, err = sortedset.FromObject(obj) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + } else { + ss = sortedset.New() + } + + added := 0 + for i := startIdx; i < len(args); i += 3 { + longitude, err := strconv.ParseFloat(args[i], 64) + if err != nil || math.IsNaN(longitude) || longitude < -180 || longitude > 180 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral("invalid longitude"), + } + } + + latitude, err := strconv.ParseFloat(args[i+1], 64) + if err != nil || math.IsNaN(latitude) || latitude < -85.05112878 || latitude > 85.05112878 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrGeneral("invalid latitude"), + } + } + + member := args[i+2] + _, exists := ss.Get(member) + + // Handle XX option: Only update existing elements + if xx && !exists { + continue + } + + // Handle NX option: Only add new elements + if nx && exists { + continue + } + + hash := geo.EncodeHash(latitude, longitude) + + wasInserted := ss.Upsert(hash, member) + if wasInserted { + added++ + } + } + + obj = store.NewObj(ss, -1, object.ObjTypeSortedSet, object.ObjEncodingBTree) + store.Put(key, obj) + + return &EvalResponse{ + Result: added, + Error: nil, + } +} + +func evalGEODIST(args []string, store *dstore.Store) *EvalResponse { + if len(args) < 3 || len(args) > 4 { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongArgumentCount("GEODIST"), + } + } + + key := args[0] + member1 := args[1] + member2 := args[2] + unit := "m" + if len(args) == 4 { + unit = strings.ToLower(args[3]) + } + + // Get the sorted set + obj := store.Get(key) + if obj == nil { + return &EvalResponse{ + Result: clientio.NIL, + Error: nil, + } + } + ss, err := sortedset.FromObject(obj) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + // Get the scores (geohashes) for both members + score1, ok := ss.Get(member1) + if !ok { + return &EvalResponse{ + Result: nil, + Error: nil, + } + } + score2, ok := ss.Get(member2) + if !ok { + return &EvalResponse{ + Result: nil, + Error: nil, + } + } + + lat1, lon1 := geo.DecodeHash(score1) + lat2, lon2 := geo.DecodeHash(score2) + + distance := geo.GetDistance(lon1, lat1, lon2, lat2) + + result, err := geo.ConvertDistance(distance, unit) + + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrWrongTypeOperation, + } + } + + return &EvalResponse{ + Result: utils.RoundToDecimals(result, 4), + Error: nil, + } +} diff --git a/internal/server/cmd_meta.go b/internal/server/cmd_meta.go index 5f0677d7e..1b26c8c4f 100644 --- a/internal/server/cmd_meta.go +++ b/internal/server/cmd_meta.go @@ -435,6 +435,14 @@ var ( Cmd: "RESTORE", CmdType: SingleShard, } + geoaddCmdMeta = CmdsMeta{ + Cmd: "GEOADD", + CmdType: SingleShard, + } + geodistCmdMeta = CmdsMeta{ + Cmd: "GEODIST", + CmdType: SingleShard, + } // Metadata for multishard commands would go here. // These commands require both breakup and gather logic. @@ -559,5 +567,7 @@ func init() { WorkerCmdsMeta["HGETALL"] = hGetAllCmdMeta WorkerCmdsMeta["DUMP"] = dumpCmdMeta WorkerCmdsMeta["RESTORE"] = restoreCmdMeta + WorkerCmdsMeta["GEOADD"] = geoaddCmdMeta + WorkerCmdsMeta["GEODIST"] = geodistCmdMeta // Additional commands (multishard, custom) can be added here as needed. } diff --git a/internal/worker/cmd_meta.go b/internal/worker/cmd_meta.go index 88cb7a0a8..aeaea41c7 100644 --- a/internal/worker/cmd_meta.go +++ b/internal/worker/cmd_meta.go @@ -156,6 +156,8 @@ const ( CmdSmembers = "SMEMBERS" CmdDump = "DUMP" CmdRestore = "RESTORE" + CmdGeoAdd = "GEOADD" + CmdGeoDist = "GEODIST" ) // Watch commands @@ -464,6 +466,13 @@ var CommandsMeta = map[string]CmdMeta{ CmdRestore: { CmdType: SingleShard, }, + // geoCommands + CmdGeoAdd: { + CmdType: SingleShard, + }, + CmdGeoDist: { + CmdType: SingleShard, + }, // Multi-shard commands. CmdRename: { diff --git a/internal/worker/worker.go b/internal/worker/worker.go index 5d05b7c1d..1bb6c9fe5 100644 --- a/internal/worker/worker.go +++ b/internal/worker/worker.go @@ -221,7 +221,8 @@ func (w *BaseWorker) executeCommand(ctx context.Context, diceDBCmd *cmd.DiceDBCm if err != nil { var workerErr error // Check if it's a CustomError - if customErr, ok := err.(*diceerrors.PreProcessError); ok { + var customErr *diceerrors.PreProcessError + if errors.As(err, &customErr) { workerErr = w.ioHandler.Write(ctx, customErr.Result) } else { workerErr = w.ioHandler.Write(ctx, err)