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

Implement EXISTS command #160

Merged
merged 5 commits into from
Jan 11, 2025
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ Benchmark script options:
* [DECR](https://sugardb.io/docs/commands/generic/decr)
* [DECRBY](https://sugardb.io/docs/commands/generic/decrby)
* [DEL](https://sugardb.io/docs/commands/generic/del)
* [EXISTS](https://sugardb.io/docs/commands/generic/exists)
* [EXPIRE](https://sugardb.io/docs/commands/generic/expire)
* [EXPIRETIME](https://sugardb.io/docs/commands/generic/expiretime)
* [FLUSHALL](https://sugardb.io/docs/commands/generic/flushall)
Expand Down
1,961 changes: 985 additions & 976 deletions coverage/coverage.out

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions docs/docs/commands/generic/exists.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# EXISTS

### Syntax
```
EXISTS
```

### Module
<span className="acl-category">generic</span>

### Categories
<span className="acl-category">fast</span>
<span className="acl-category">read</span>
<span className="acl-category">keyspace</span>

### Description
Returns the number of keys that exists from the provided list of keys. Note: If duplicate keys are provided, each one is counted separately.

### Examples

<Tabs
defaultValue="go"
values={[
{ label: 'Go (Embedded)', value: 'go', },
{ label: 'CLI', value: 'cli', },
]}
>
<TabItem value="go">
Return the number of keys that exists:
```go
db, err := sugardb.NewSugarDB()
if err != nil {
log.Fatal(err)
}
key, err := db.Exists("key1")
```
</TabItem>
<TabItem value="cli">
Return the number of keys that exists:
```
> EXISTS key1 key2
```
</TabItem>
</Tabs>
28 changes: 28 additions & 0 deletions internal/modules/generic/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,24 @@ func handleRenamenx(params internal.HandlerFuncParams) ([]byte, error) {
return handleRename(params)
}

func handleExists(params internal.HandlerFuncParams) ([]byte, error) {
keys, err := existsKeyFunc(params.Command)
if err != nil {
return nil, err
}

// check if key exists and count
existingKeys := params.KeysExist(params.Context, keys.ReadKeys)
keyCount := 0
for _, key := range keys.ReadKeys {
if existingKeys[key] {
keyCount++
}
}

return []byte(fmt.Sprintf(":%d\r\n", keyCount)), nil
}

func handleFlush(params internal.HandlerFuncParams) ([]byte, error) {
if len(params.Command) != 1 {
return nil, errors.New(constants.WrongArgsResponse)
Expand Down Expand Up @@ -1393,5 +1411,15 @@ The REPLACE option removes the destination key before copying the value to it.`,
KeyExtractionFunc: renamenxKeyFunc,
HandlerFunc: handleRenamenx,
},
{
Command: "exists",
Module: constants.GenericModule,
Categories: []string{constants.KeyspaceCategory, constants.ReadCategory, constants.FastCategory},
Description: "(EXISTS key [key ...]) Returns the number of keys that exist from the provided list of keys.",
Sync: false,
Type: "BUILT_IN",
KeyExtractionFunc: existsKeyFunc,
HandlerFunc: handleExists,
},
}
}
112 changes: 112 additions & 0 deletions internal/modules/generic/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2391,6 +2391,118 @@ func Test_Generic(t *testing.T) {
}
})

t.Run("Test_HandleEXISTS", func(t *testing.T) {
t.Parallel()

conn, err := internal.GetConnection("localhost", port)
if err != nil {
t.Error(err)
return
}
defer func() {
_ = conn.Close()
}()
client := resp.NewConn(conn)

tests := []struct {
name string
presetKeys map[string]string
checkKeys []string
expectedResponse int
}{
{
name: "1. Key doesn't exist",
presetKeys: map[string]string{},
checkKeys: []string{"nonExistentKey"},
expectedResponse: 0,
},
{
name: "2. Key exists",
presetKeys: map[string]string{"existentKey": "value"},
checkKeys: []string{"existentKey"},
expectedResponse: 1,
},
{
name: "3. All keys exist",
presetKeys: map[string]string{
"key1": "value1",
"key2": "value2",
"key3": "value3",
},
checkKeys: []string{"key1", "key2", "key3"},
expectedResponse: 3,
},
{
name: "4. Only some keys exist",
presetKeys: map[string]string{
"key1": "value1",
"key2": "value2",
},
checkKeys: []string{"key1", "key2", "nonExistentKey"},
expectedResponse: 2,
},
{
name: "5. All keys exist with duplicates",
presetKeys: map[string]string{
"key1": "value1",
"key2": "value2",
"key3": "value3",
},
checkKeys: []string{"key1", "key2", "key3", "key1", "key2"},
expectedResponse: 5,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Preset keys
for key, value := range test.presetKeys {
command := []resp.Value{resp.StringValue("SET"), resp.StringValue(key), resp.StringValue(value)}
if err = client.WriteArray(command); err != nil {
t.Error(err)
return
}
res, _, err := client.ReadValue()
if err != nil {
t.Error(err)
return
}
if !strings.EqualFold(res.String(), "OK") {
t.Errorf("expected preset response to be OK, got %s", res.String())
return
}
}

// Check EXISTS command
existsCommand := []resp.Value{resp.StringValue("EXISTS")}
for _, key := range test.checkKeys {
existsCommand = append(existsCommand, resp.StringValue(key))
}

if err = client.WriteArray(existsCommand); err != nil {
t.Error(err)
return
}

res, _, err := client.ReadValue()
if err != nil {
t.Error(err)
return
}

actualCount, err := strconv.Atoi(res.String())
if err != nil {
t.Errorf("error parsing response to int: %s", err)
return
}

if actualCount != test.expectedResponse {
t.Errorf("expected %d existing keys, got %d", test.expectedResponse, actualCount)
}
})
}
})

t.Run("Test_HandlerDECRBY", func(t *testing.T) {
t.Parallel()
conn, err := internal.GetConnection("localhost", port)
Expand Down
11 changes: 11 additions & 0 deletions internal/modules/generic/key_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,14 @@ func moveKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) {
WriteKeys: []string{cmd[1]},
}, nil
}

func existsKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) {
if len(cmd) < 2 {
return internal.KeyExtractionFuncResult{}, errors.New(constants.WrongArgsResponse)
}
return internal.KeyExtractionFuncResult{
Channels: make([]string, 0),
ReadKeys: cmd[1:],
WriteKeys: make([]string, 0),
}, nil
}
16 changes: 16 additions & 0 deletions sugardb/api_generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -806,3 +806,19 @@ func (server *SugarDB) Move(key string, destinationDB int) (int, error) {
}
return internal.ParseIntegerResponse(b)
}

// Exists returns the number of keys that exist from the provided list of keys.
// Note: Duplicate keys in the argument list are each counted separately.
//
// Parameters:
//
// `keys` - ...string - the keys whose existence should be checked.
//
// Returns: An integer representing the number of keys that exist.
func (server *SugarDB) Exists(keys ...string) (int, error) {
b, err := server.handleCommand(server.context, internal.EncodeCommand(append([]string{"EXISTS"}, keys...)), nil, false, true)
if err != nil {
return 0, err
}
return internal.ParseIntegerResponse(b)
}
33 changes: 33 additions & 0 deletions sugardb/api_generic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1410,6 +1410,39 @@ func TestSugarDB_RANDOMKEY(t *testing.T) {

}

func TestSugarDB_Exists(t *testing.T) {
server := createSugarDB()

// Test with no keys
keys := []string{"key1", "key2", "key3"}
existsCount, err := server.Exists(keys...)
if err != nil {
t.Error(err)
return
}
if existsCount != 0 {
t.Errorf("EXISTS error, expected 0, got %d", existsCount)
}

// Test with some keys
for _, k := range keys {
err := presetValue(server, context.Background(), k, "")
if err != nil {
t.Error(err)
return
}
}

existsCount, err = server.Exists(keys...)
if err != nil {
t.Error(err)
return
}
if existsCount != len(keys) {
t.Errorf("EXISTS error, expected %d, got %d", len(keys), existsCount)
}
}

func TestSugarDB_DBSize(t *testing.T) {
server := createSugarDB()
got, err := server.DBSize()
Expand Down
Loading