Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LINDEX implementation added #1395

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions docs/src/content/docs/commands/LINDEX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
title: LINDEX
description: The `LINDEX` command in DiceDB is used to find an element present in the list stored at a key.
---

# LINDEX

The `LINDEX` command in DiceDB is used to find an element present in the list stored at a key. If the key does not exist or the index specified is out of range of the list then the command will throw an error.

## Command Syntax

```bash
LINDEX [key] [index]
```

## Parameters

| Parameter | Description | Type | Required |
| ------------ | ----------------------------------------------------------------------------------- | -------- | -------- |
| KEY | The key associated with the list for which the element you want to retrieve. | String | Yes |
| INDEX | The index or position of the element we want to retrieve in the list. 0-based indexing is used to consider elements from head or start of the list. Negative indexing is used (starts from -1) to consider elements from tail or end of the list. | Integer | Yes |

## Return Values

| Condition | Return Value |
| --------------------------------------------- | --------------------------------------------- |
| Command is successful | Returns the element present in that index |
| Key does not exist | error |
| Syntax or specified constraints are invalid | error |

## Behaviour

When the `LINDEX` command is executed, it performs the specified subcommand operation -

- Returns element present at the `index` of the list associated with the `key` provided as arguments of the command.

- If the `key` exists but is not associated with the list, an error is returned.

## Errors

- `Non existent key`:

- Error Message: `ERR could not perform this operation on a key that doesn't exist`

- `Missing Arguments`:

- Error Message: `ERR wrong number of arguments for 'latency subcommand' command`
- If required arguments for a subcommand are missing, DiceDB will return an error.

- `Key not holding a list`

- Error Message : `WRONGTYPE Operation against a key holding the wrong kind of value`

## Example Usage

### Basic Usage

```
dicedb> LPUSH k 1
1
dicedb> LPUSH k 2
2
dicedb> LINDEX k 0
2
dicedb> LINDEX k -1
1
```

### Index out of range

```
dicedb> LINDEX k 3
Error: ERR Index out of range
```

### Non-Existent Key

```
dicedb> LINDEX NON-EXISTENT -1
Error: ERR could not perform this operation on a key that doesn't exist
```

## Best Practices

- Check Key Type: Before using `LINDEX`, ensure that the key is associated with a list to avoid errors.

- Handle Non-Existent Keys: Be prepared to handle the case where the key does not exist, as `LINDEX` will return `ERROR` in such scenarios.

- Make sure the index is within range of the list.
76 changes: 76 additions & 0 deletions integration_tests/commands/http/deque_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -615,3 +615,79 @@ func TestLPOPCount(t *testing.T) {

exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"keys": [...]string{"k"}}})
}

func TestLIndex(t *testing.T) {
exec := NewHTTPCommandExecutor()
exec.FireCommand(HTTPCommand{Command: "FLUSHDB"})

testcases := []struct {
name string
cmds []HTTPCommand
expects []any
}{
{
name: "LINDEX for string values",
cmds: []HTTPCommand{
{Command: "RPUSH", Body: map[string]interface{}{"key": "k", "value": "v1"}},
{Command: "RPUSH", Body: map[string]interface{}{"key": "k", "value": "v2"}},
{Command: "RPUSH", Body: map[string]interface{}{"key": "k", "value": "v3"}},
{Command: "RPUSH", Body: map[string]interface{}{"key": "k", "value": "v4"}},
{Command: "LINDEX", Body: map[string]interface{}{"key": "k", "value": 0}},
{Command: "LINDEX", Body: map[string]interface{}{"key": "k", "value": 2}},
{Command: "LINDEX", Body: map[string]interface{}{"key": "k", "value": 3}},
{Command: "LINDEX", Body: map[string]interface{}{"key": "k", "value": -1}},
{Command: "LINDEX", Body: map[string]interface{}{"key": "k", "value": -4}},
{Command: "LINDEX", Body: map[string]interface{}{"key": "k", "value": -3}},
},
expects: []interface{}{
float64(1),
float64(2),
float64(3),
float64(4),
"v1",
"v3",
"v4",
"v4",
"v1",
"v2",
},
},
{
name: "LINDEX for int values",
cmds: []HTTPCommand{
{Command: "RPUSH", Body: map[string]interface{}{"key": "k", "value": 1}},
{Command: "RPUSH", Body: map[string]interface{}{"key": "k", "value": 2}},
{Command: "RPUSH", Body: map[string]interface{}{"key": "k", "value": 3}},
{Command: "RPUSH", Body: map[string]interface{}{"key": "k", "value": 4}},
{Command: "LINDEX", Body: map[string]interface{}{"key": "k", "value": 0}},
{Command: "LINDEX", Body: map[string]interface{}{"key": "k", "value": 2}},
{Command: "LINDEX", Body: map[string]interface{}{"key": "k", "value": 3}},
{Command: "LINDEX", Body: map[string]interface{}{"key": "k", "value": -1}},
{Command: "LINDEX", Body: map[string]interface{}{"key": "k", "value": -4}},
{Command: "LINDEX", Body: map[string]interface{}{"key": "k", "value": -3}},
},
expects: []interface{}{
float64(1),
float64(2),
float64(3),
float64(4),
"1",
"3",
"4",
"4",
"1",
"2",
},
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"keys": []interface{}{"k"}}})
for i, cmd := range tc.cmds {
result, _ := exec.FireCommand(cmd)
assert.Equal(t, tc.expects[i], result, "Value mismatch for cmd %v", cmd)
}
})
}
}
35 changes: 35 additions & 0 deletions integration_tests/commands/resp/deque_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"testing"
"time"

diceerrors "github.com/dicedb/dice/internal/errors"

"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -501,6 +503,39 @@ func TestLInsert(t *testing.T) {
deqCleanUp(conn, "k")
}

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

testcases := []struct {
name string
cmds []string
expect []any
}{
{
name: "LINDEX from start",
cmds: []string{"RPUSH k v1 v2 v3 v4", "LINDEX k 0", "LINDEX k 1", "LINDEX k 2", "LINDEX k 3", "LINDEX k 4"},
expect: []any{int64(4), "v1", "v2", "v3", "v4", diceerrors.ErrIndexOutOfRange.Error()},
},
{
name: "LINDEX from end",
cmds: []string{"LINDEX k -1", "LINDEX k -2", "LINDEX k -3", "LINDEX k -4", "LINDEX k -5"},
expect: []any{"v4", "v3", "v2", "v1", diceerrors.ErrIndexOutOfRange.Error()},
},
}

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

deqCleanUp(conn, "k")
}

func TestLRange(t *testing.T) {
conn := getLocalConnection()
defer conn.Close()
Expand Down
46 changes: 46 additions & 0 deletions integration_tests/commands/websocket/deque_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,52 @@ func TestLPush(t *testing.T) {
DeleteKey(t, conn, exec, "k")
}

func TestLIndex(t *testing.T) {
exec := NewWebsocketCommandExecutor()

testCases := []struct {
name string
cmds []string
expect []any
}{
{
name: "LINDEX",
cmds: []string{
"RPUSH k v1 v2 v3 v4",
"LINDEX k 0",
"LINDEX k 2",
"LINDEX k 3",
"LINDEX k -1",
"LINDEX k -4",
"LINDEX k -3",
},
expect: []any{
float64(4),
"v1",
"v3",
"v4",
"v4",
"v1",
"v2",
},
},
}

conn := exec.ConnectToServer()
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.NilError(t, err)
assert.Equal(t, tc.expect[i], result, "Value mismatch for cmd %s", cmd)
}
})
}

DeleteKey(t, conn, exec, "k")
}

func TestRPush(t *testing.T) {
deqNormalValues, deqEdgeValues := deqTestInit()
exec := NewWebsocketCommandExecutor()
Expand Down
4 changes: 4 additions & 0 deletions internal/commandhandler/cmd_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ const (
CmdGetDel = "GETDEL"
CmdLrange = "LRANGE"
CmdLinsert = "LINSERT"
CmdLindex = "LINDEX"
CmdJSONArrInsert = "JSON.ARRINSERT"
CmdJSONArrTrim = "JSON.ARRTRIM"
CmdJSONArrAppend = "JSON.ARRAPPEND"
Expand Down Expand Up @@ -429,6 +430,9 @@ var CommandsMeta = map[string]CmdMeta{
CmdLLEN: {
CmdType: SingleShard,
},
CmdLindex: {
CmdType: SingleShard,
},
CmdCMSQuery: {
CmdType: SingleShard,
},
Expand Down
1 change: 1 addition & 0 deletions internal/errors/migrated_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ var (
ErrIntegerOutOfRange = errors.New("ERR value is not an integer or out of range") // Represents a value that is either not an integer or is out of allowed range.
ErrInvalidNumberFormat = errors.New("ERR value is not an integer or a float") // Signals that a value provided is not in a valid integer or float format.
ErrValueOutOfRange = errors.New("ERR value is out of range") // Indicates that a value is beyond the permissible range.
ErrIndexOutOfRange = errors.New("ERR Index out of range") // Indicates that index given by the user is out of range
ErrOverflow = errors.New("ERR increment or decrement would overflow") // Signifies that an increment or decrement operation would exceed the limits.
ErrSyntax = errors.New("ERR syntax error") // Represents a syntax error in a DiceDB command.
ErrKeyNotFound = errors.New("ERR no such key") // Indicates that the specified key does not exist.
Expand Down
20 changes: 20 additions & 0 deletions internal/eval/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,25 @@ var (
Arity: 4,
KeySpecs: KeySpecs{BeginIndex: 1},
}
lindexCmdMeta = DiceCmdMeta {
Name: "LINDEX",
Info: `
Usage:
LINDEX key index
Info:
Returns element stored at index in the list stored at a key.
Indexing is 0 based.

Index can be negative. Negative index can be used to start from the end of the list i.e., index = -1 means last element of the list, index = -2 means second last element of the list and so on.

Returns:
Element value present at that index.
`,
NewEval: evalLINDEX,
IsMigrated: true,
Arity: 2,
KeySpecs: KeySpecs{BeginIndex: 1},
}
)

func init() {
Expand Down Expand Up @@ -1485,6 +1504,7 @@ func init() {
DiceCmds["LINSERT"] = linsertCmdMeta
DiceCmds["LRANGE"] = lrangeCmdMeta
DiceCmds["JSON.ARRINDEX"] = jsonArrIndexCmdMeta
DiceCmds["LINDEX"] = lindexCmdMeta

DiceCmds["SINGLETOUCH"] = singleTouchCmdMeta
DiceCmds["SINGLEDBSIZE"] = singleDBSizeCmdMeta
Expand Down
9 changes: 9 additions & 0 deletions internal/eval/deque.go
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,15 @@ func (i *DequeIterator) Next() (string, error) {
return x, nil
}

func (i *DequeIterator) Value() (string, error) {
if i.ElementsTraversed == i.deque.Length {
return "(nil)", fmt.Errorf("iterator exhausted")
}

x, _ := DecodeDeqEntry(i.CurrentNode.buf[i.BufIndex:])
return x, nil
}

// *************************** deque entry encode/decode ***************************

// EncodeDeqEntry encodes `x` into an entry of Deque. An entry will be encoded as [enc + data + backlen].
Expand Down
Loading
Loading