Skip to content

Commit

Permalink
Merge branch 'master' of github.com:dicedb/dice
Browse files Browse the repository at this point in the history
  • Loading branch information
arpitbbhayani committed Dec 11, 2024
2 parents fe75261 + df110f6 commit 1fb6e44
Show file tree
Hide file tree
Showing 8 changed files with 390 additions and 0 deletions.
58 changes: 58 additions & 0 deletions docs/src/content/docs/commands/GEOPOS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: GEOPOS
description: The `GEOPOS` command in DiceDB is used to return the longitude, latitude to a specified key, as stored in the sorted set.
---

The `GEOPOS` command in DiceDB is used to return the longitude, latitude to a specified key which is stored in a sorted set. When elements are added via `GEOADD` then they are stored in 52 bit geohash hence the values returned by `GEOPOS` might have small margins of error.

## Syntax

```bash
GEOPOS key [member [member ...]]
```
## Parameters
| Parameter | Description | Type | Required |
| --------- | --------------------------------------------------------------------------------- | ------ | -------- |
| key | The name of the sorted set key whose member's coordinates are to be returned | string | Yes |
| member | A unique identifier for the location. | string | Yes |
## Return Values
| Condition | Return Value |
| ------------------------------------------------------------ | ----------------------------------------------------------- |
| Coordinates exist for the specified member(s) | Returns an ordered list of coordinates (longitude, latitude) for each specified member |
| Coordinates do not exist for the specified member(s) | Returns `(nil)` for each member without coordinates
| Incorrect Argument Count |`ERR wrong number of arguments for 'geopos' command` |
| Key does not exist in the sorted set |`Error: nil` |
## Behaviour
When the GEOPOS command is issued, DiceDB performs the following steps:
1. It checks if argument count is valid or not. If not an error is thrown.
2. It checks the validity of the key.
3. If the key is invalid then an error is returned.
4. Else it checks the members provided after the key.
5. For each member it checks the coordinates of the member.
6. If the coordinates exist then it is returned in an ordered list of latitude, longitude for the particular member.
7. If the coordinates do not exist then a ``(nil)`` is returned for that member.
## Errors
1. `Wrong number of arguments for 'GEOPOS' 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. `Wrong key for 'GEOPOS' command`
- Error Message: Error: nil
- Occurs when the command is executed with a key that does not exist in the sorted set.
## Example Usage
Here are a few examples demonstrating the usage of the GEOPOS command:
### Example: Fetching the latitude, longitude of an existing member of the set
```bash
127.0.0.1:7379> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
2
127.0.0.1:7379> GEOPOS Sicily "Palermo"
1) 1) 13.361387
2) 38.115556
```
### Example: Fetching the latitude, longitude of a member not in the set
```bash
127.0.0.1:7379> GEOPOS Sicily "Agrigento"
1) (nil)
```
56 changes: 56 additions & 0 deletions integration_tests/commands/http/geo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,59 @@ func TestGeoDist(t *testing.T) {
})
}
}

func TestGeoPos(t *testing.T) {
exec := NewHTTPCommandExecutor()

testCases := []struct {
name string
commands []HTTPCommand
expected []interface{}
}{
{
name: "GEOPOS for existing points",
commands: []HTTPCommand{
{Command: "GEOADD", Body: map[string]interface{}{"key": "index", "values": []interface{}{"13.361389", "38.115556", "Palermo"}}},
{Command: "GEOPOS", Body: map[string]interface{}{"key": "index", "values": []interface{}{"Palermo"}}},
},
expected: []interface{}{
float64(1),
[]interface{}{[]interface{}{float64(13.361387), float64(38.115556)}},
},
},
{
name: "GEOPOS for non-existing points",
commands: []HTTPCommand{
{Command: "GEOPOS", Body: map[string]interface{}{"key": "index", "values": []interface{}{"NonExisting"}}},
},
expected: []interface{}{[]interface{}{nil}},
},
{
name: "GEOPOS for non-existing index",
commands: []HTTPCommand{
{Command: "GEOPOS", Body: map[string]interface{}{"key": "NonExisting", "values": []interface{}{"Palermo"}}},
},
expected: []interface{}{nil},
},
{
name: "GEOPOS for a key not used for setting geospatial values",
commands: []HTTPCommand{
{Command: "SET", Body: map[string]interface{}{"key": "k", "value": "v"}},
{Command: "GEOPOS", Body: map[string]interface{}{"key": "k", "values": []interface{}{"v"}}},
},
expected: []interface{}{
"OK",
"WRONGTYPE Operation against a key holding the wrong kind of value",
},
},
}

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 %v", cmd)
}
})
}
}
60 changes: 60 additions & 0 deletions integration_tests/commands/resp/geo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,63 @@ func TestGeoDist(t *testing.T) {
})
}
}

func TestGeoPos(t *testing.T) {
conn := getLocalConnection()
defer conn.Close()

testCases := []struct {
name string
cmds []string
expect []interface{}
delays []time.Duration
}{
{
name: "GEOPOS b/w existing points",
cmds: []string{
"GEOADD index 13.361389 38.115556 Palermo",
"GEOPOS index Palermo",
},
expect: []interface{}{
int64(1),
[]interface{}{[]interface{}{"13.361387", "38.115556"}},
},
},
{
name: "GEOPOS for non existing points",
cmds: []string{
"GEOPOS index NonExisting",
},
expect: []interface{}{
[]interface{}{"(nil)"},
},
},
{
name: "GEOPOS for non existing index",
cmds: []string{
"GEOPOS NonExisting Palermo",
},
expect: []interface{}{"(nil)"},
},
{
name: "GEOPOS for a key not used for setting geospatial values",
cmds: []string{
"SET k v",
"GEOPOS k v",
},
expect: []interface{}{
"OK",
"WRONGTYPE Operation against a key holding the wrong kind of value",
},
},
}

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)
}
})
}
}
60 changes: 60 additions & 0 deletions integration_tests/commands/websocket/geo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,63 @@ func TestGeoDist(t *testing.T) {
})
}
}

func TestGeoPos(t *testing.T) {
exec := NewWebsocketCommandExecutor()
conn := exec.ConnectToServer()
defer conn.Close()

testCases := []struct {
name string
cmds []string
expect []interface{}
}{
{
name: "GEOPOS b/w existing points",
cmds: []string{
"GEOADD index 13.361389 38.115556 Palermo",
"GEOPOS index Palermo",
},
expect: []interface{}{
float64(1),
[]interface{}{[]interface{}{float64(13.361387), float64(38.115556)}},
},
},
{
name: "GEOPOS for non existing points",
cmds: []string{
"GEOPOS index NonExisting",
},
expect: []interface{}{[]interface{}{nil}},
},
{
name: "GEOPOS for non existing index",
cmds: []string{
"GEOPOS NonExisting Palermo",
},
expect: []interface{}{nil},
},
{
name: "GEOPOS for a key not used for setting geospatial values",
cmds: []string{
"SET k v",
"GEOPOS k v",
},
expect: []interface{}{
"OK",
"WRONGTYPE Operation against a key holding the wrong kind of value",
},
},
}

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)
}
})
}
}

9 changes: 9 additions & 0 deletions internal/eval/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,14 @@ var (
NewEval: evalGEODIST,
KeySpecs: KeySpecs{BeginIndex: 1},
}
geoPosCmdMeta = DiceCmdMeta{
Name: "GEOPOS",
Info: `Returns the latitude and longitude of the members identified by the particular index.`,
Arity: -3,
NewEval: evalGEOPOS,
IsMigrated: true,
KeySpecs: KeySpecs{BeginIndex: 1},
}
jsonstrappendCmdMeta = DiceCmdMeta{
Name: "JSON.STRAPPEND",
Info: `JSON.STRAPPEND key [path] value
Expand Down Expand Up @@ -1354,6 +1362,7 @@ func init() {
DiceCmds["FLUSHDB"] = flushdbCmdMeta
DiceCmds["GEOADD"] = geoAddCmdMeta
DiceCmds["GEODIST"] = geoDistCmdMeta
DiceCmds["GEOPOS"] = geoPosCmdMeta
DiceCmds["GET"] = getCmdMeta
DiceCmds["GETBIT"] = getBitCmdMeta
DiceCmds["GETDEL"] = getDelCmdMeta
Expand Down
91 changes: 91 additions & 0 deletions internal/eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ func TestEval(t *testing.T) {
testEvalBitFieldRO(t, store)
testEvalGEOADD(t, store)
testEvalGEODIST(t, store)
testEvalGEOPOS(t, store)
testEvalJSONSTRAPPEND(t, store)
testEvalINCR(t, store)
testEvalINCRBY(t, store)
Expand Down Expand Up @@ -8247,6 +8248,96 @@ func testEvalGEODIST(t *testing.T, store *dstore.Store) {
runMigratedEvalTests(t, tests, evalGEODIST, store)
}

func testEvalGEOPOS(t *testing.T, store *dstore.Store) {
tests := map[string]evalTestCase{
"GEOPOS for existing single point": {
setup: func() {
evalGEOADD([]string{"index", "13.361387", "38.115556", "Palermo"}, store)
},
input: []string{"index", "Palermo"},
migratedOutput: EvalResponse{
Result: []interface{}{[]interface{}{float64(13.361387), float64(38.115556)}},
Error: nil,
},
},
"GEOPOS for multiple existing points": {
setup: func() {
evalGEOADD([]string{"points", "13.361387", "38.115556", "Palermo"}, store)
evalGEOADD([]string{"points", "15.087265", "37.502668", "Catania"}, store)
},
input: []string{"points", "Palermo", "Catania"},
migratedOutput: EvalResponse{
Result: []interface{}{
[]interface{}{float64(13.361387), float64(38.115556)},
[]interface{}{float64(15.087265), float64(37.502668)},
},
Error: nil,
},
},
"GEOPOS for a point that does not exist": {
setup: func() {
evalGEOADD([]string{"index", "13.361387", "38.115556", "Palermo"}, store)
},
input: []string{"index", "NonExisting"},
migratedOutput: EvalResponse{
Result: []interface{}{nil},
Error: nil,
},
},
"GEOPOS for multiple points, one existing and one non-existing": {
setup: func() {
evalGEOADD([]string{"index", "13.361387", "38.115556", "Palermo"}, store)
},
input: []string{"index", "Palermo", "NonExisting"},
migratedOutput: EvalResponse{
Result: []interface{}{
[]interface{}{float64(13.361387), float64(38.115556)},
nil,
},
Error: nil,
},
},
"GEOPOS for empty index": {
setup: func() {
evalGEOADD([]string{"", "13.361387", "38.115556", "Palermo"}, store)
},
input: []string{"", "Palermo"},
migratedOutput: EvalResponse{
Result: []interface{}{
[]interface{}{float64(13.361387), float64(38.115556)},
},
Error: nil,
},
},
"GEOPOS with no members in key": {
input: []string{"index", "Palermo"},
migratedOutput: EvalResponse{
Result: clientio.NIL,
Error: nil,
},
},
"GEOPOS with invalid number of arguments": {
input: []string{"index"},
migratedOutput: EvalResponse{
Result: nil,
Error: diceerrors.ErrWrongArgumentCount("GEOPOS"),
},
},
"GEOPOS for a key not used for setting geospatial values": {
setup: func() {
evalSET([]string{"k", "v"}, store)
},
input: []string{"k", "v"},
migratedOutput: EvalResponse{
Result: nil,
Error: errors.New("WRONGTYPE Operation against a key holding the wrong kind of value"),
},
},
}

runMigratedEvalTests(t, tests, evalGEOPOS, store)
}

func testEvalJSONSTRAPPEND(t *testing.T, store *dstore.Store) {
tests := map[string]evalTestCase{
"append to single field": {
Expand Down
Loading

0 comments on commit 1fb6e44

Please sign in to comment.