Skip to content

Commit 40527b1

Browse files
authored
Implement EXISTS command (#160)
Implemented EXISTS command - @NicoleStrel
1 parent 8f1330e commit 40527b1

File tree

8 files changed

+1233
-976
lines changed

8 files changed

+1233
-976
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ Benchmark script options:
210210
* [DECR](https://sugardb.io/docs/commands/generic/decr)
211211
* [DECRBY](https://sugardb.io/docs/commands/generic/decrby)
212212
* [DEL](https://sugardb.io/docs/commands/generic/del)
213+
* [EXISTS](https://sugardb.io/docs/commands/generic/exists)
213214
* [EXPIRE](https://sugardb.io/docs/commands/generic/expire)
214215
* [EXPIRETIME](https://sugardb.io/docs/commands/generic/expiretime)
215216
* [FLUSHALL](https://sugardb.io/docs/commands/generic/flushall)

coverage/coverage.out

Lines changed: 985 additions & 976 deletions
Large diffs are not rendered by default.

docs/docs/commands/generic/exists.mdx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import Tabs from '@theme/Tabs';
2+
import TabItem from '@theme/TabItem';
3+
4+
# EXISTS
5+
6+
### Syntax
7+
```
8+
EXISTS
9+
```
10+
11+
### Module
12+
<span className="acl-category">generic</span>
13+
14+
### Categories
15+
<span className="acl-category">fast</span>
16+
<span className="acl-category">read</span>
17+
<span className="acl-category">keyspace</span>
18+
19+
### Description
20+
Returns the number of keys that exists from the provided list of keys. Note: If duplicate keys are provided, each one is counted separately.
21+
22+
### Examples
23+
24+
<Tabs
25+
defaultValue="go"
26+
values={[
27+
{ label: 'Go (Embedded)', value: 'go', },
28+
{ label: 'CLI', value: 'cli', },
29+
]}
30+
>
31+
<TabItem value="go">
32+
Return the number of keys that exists:
33+
```go
34+
db, err := sugardb.NewSugarDB()
35+
if err != nil {
36+
log.Fatal(err)
37+
}
38+
key, err := db.Exists("key1")
39+
```
40+
</TabItem>
41+
<TabItem value="cli">
42+
Return the number of keys that exists:
43+
```
44+
> EXISTS key1 key2
45+
```
46+
</TabItem>
47+
</Tabs>

internal/modules/generic/commands.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,24 @@ func handleRenamenx(params internal.HandlerFuncParams) ([]byte, error) {
692692
return handleRename(params)
693693
}
694694

695+
func handleExists(params internal.HandlerFuncParams) ([]byte, error) {
696+
keys, err := existsKeyFunc(params.Command)
697+
if err != nil {
698+
return nil, err
699+
}
700+
701+
// check if key exists and count
702+
existingKeys := params.KeysExist(params.Context, keys.ReadKeys)
703+
keyCount := 0
704+
for _, key := range keys.ReadKeys {
705+
if existingKeys[key] {
706+
keyCount++
707+
}
708+
}
709+
710+
return []byte(fmt.Sprintf(":%d\r\n", keyCount)), nil
711+
}
712+
695713
func handleFlush(params internal.HandlerFuncParams) ([]byte, error) {
696714
if len(params.Command) != 1 {
697715
return nil, errors.New(constants.WrongArgsResponse)
@@ -1393,5 +1411,15 @@ The REPLACE option removes the destination key before copying the value to it.`,
13931411
KeyExtractionFunc: renamenxKeyFunc,
13941412
HandlerFunc: handleRenamenx,
13951413
},
1414+
{
1415+
Command: "exists",
1416+
Module: constants.GenericModule,
1417+
Categories: []string{constants.KeyspaceCategory, constants.ReadCategory, constants.FastCategory},
1418+
Description: "(EXISTS key [key ...]) Returns the number of keys that exist from the provided list of keys.",
1419+
Sync: false,
1420+
Type: "BUILT_IN",
1421+
KeyExtractionFunc: existsKeyFunc,
1422+
HandlerFunc: handleExists,
1423+
},
13961424
}
13971425
}

internal/modules/generic/commands_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2391,6 +2391,118 @@ func Test_Generic(t *testing.T) {
23912391
}
23922392
})
23932393

2394+
t.Run("Test_HandleEXISTS", func(t *testing.T) {
2395+
t.Parallel()
2396+
2397+
conn, err := internal.GetConnection("localhost", port)
2398+
if err != nil {
2399+
t.Error(err)
2400+
return
2401+
}
2402+
defer func() {
2403+
_ = conn.Close()
2404+
}()
2405+
client := resp.NewConn(conn)
2406+
2407+
tests := []struct {
2408+
name string
2409+
presetKeys map[string]string
2410+
checkKeys []string
2411+
expectedResponse int
2412+
}{
2413+
{
2414+
name: "1. Key doesn't exist",
2415+
presetKeys: map[string]string{},
2416+
checkKeys: []string{"nonExistentKey"},
2417+
expectedResponse: 0,
2418+
},
2419+
{
2420+
name: "2. Key exists",
2421+
presetKeys: map[string]string{"existentKey": "value"},
2422+
checkKeys: []string{"existentKey"},
2423+
expectedResponse: 1,
2424+
},
2425+
{
2426+
name: "3. All keys exist",
2427+
presetKeys: map[string]string{
2428+
"key1": "value1",
2429+
"key2": "value2",
2430+
"key3": "value3",
2431+
},
2432+
checkKeys: []string{"key1", "key2", "key3"},
2433+
expectedResponse: 3,
2434+
},
2435+
{
2436+
name: "4. Only some keys exist",
2437+
presetKeys: map[string]string{
2438+
"key1": "value1",
2439+
"key2": "value2",
2440+
},
2441+
checkKeys: []string{"key1", "key2", "nonExistentKey"},
2442+
expectedResponse: 2,
2443+
},
2444+
{
2445+
name: "5. All keys exist with duplicates",
2446+
presetKeys: map[string]string{
2447+
"key1": "value1",
2448+
"key2": "value2",
2449+
"key3": "value3",
2450+
},
2451+
checkKeys: []string{"key1", "key2", "key3", "key1", "key2"},
2452+
expectedResponse: 5,
2453+
},
2454+
}
2455+
2456+
for _, test := range tests {
2457+
t.Run(test.name, func(t *testing.T) {
2458+
// Preset keys
2459+
for key, value := range test.presetKeys {
2460+
command := []resp.Value{resp.StringValue("SET"), resp.StringValue(key), resp.StringValue(value)}
2461+
if err = client.WriteArray(command); err != nil {
2462+
t.Error(err)
2463+
return
2464+
}
2465+
res, _, err := client.ReadValue()
2466+
if err != nil {
2467+
t.Error(err)
2468+
return
2469+
}
2470+
if !strings.EqualFold(res.String(), "OK") {
2471+
t.Errorf("expected preset response to be OK, got %s", res.String())
2472+
return
2473+
}
2474+
}
2475+
2476+
// Check EXISTS command
2477+
existsCommand := []resp.Value{resp.StringValue("EXISTS")}
2478+
for _, key := range test.checkKeys {
2479+
existsCommand = append(existsCommand, resp.StringValue(key))
2480+
}
2481+
2482+
if err = client.WriteArray(existsCommand); err != nil {
2483+
t.Error(err)
2484+
return
2485+
}
2486+
2487+
res, _, err := client.ReadValue()
2488+
if err != nil {
2489+
t.Error(err)
2490+
return
2491+
}
2492+
2493+
actualCount, err := strconv.Atoi(res.String())
2494+
if err != nil {
2495+
t.Errorf("error parsing response to int: %s", err)
2496+
return
2497+
}
2498+
2499+
if actualCount != test.expectedResponse {
2500+
t.Errorf("expected %d existing keys, got %d", test.expectedResponse, actualCount)
2501+
}
2502+
})
2503+
}
2504+
})
2505+
23942506
t.Run("Test_HandlerDECRBY", func(t *testing.T) {
23952507
t.Parallel()
23962508
conn, err := internal.GetConnection("localhost", port)

internal/modules/generic/key_funcs.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,14 @@ func moveKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) {
310310
WriteKeys: []string{cmd[1]},
311311
}, nil
312312
}
313+
314+
func existsKeyFunc(cmd []string) (internal.KeyExtractionFuncResult, error) {
315+
if len(cmd) < 2 {
316+
return internal.KeyExtractionFuncResult{}, errors.New(constants.WrongArgsResponse)
317+
}
318+
return internal.KeyExtractionFuncResult{
319+
Channels: make([]string, 0),
320+
ReadKeys: cmd[1:],
321+
WriteKeys: make([]string, 0),
322+
}, nil
323+
}

sugardb/api_generic.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,3 +806,19 @@ func (server *SugarDB) Move(key string, destinationDB int) (int, error) {
806806
}
807807
return internal.ParseIntegerResponse(b)
808808
}
809+
810+
// Exists returns the number of keys that exist from the provided list of keys.
811+
// Note: Duplicate keys in the argument list are each counted separately.
812+
//
813+
// Parameters:
814+
//
815+
// `keys` - ...string - the keys whose existence should be checked.
816+
//
817+
// Returns: An integer representing the number of keys that exist.
818+
func (server *SugarDB) Exists(keys ...string) (int, error) {
819+
b, err := server.handleCommand(server.context, internal.EncodeCommand(append([]string{"EXISTS"}, keys...)), nil, false, true)
820+
if err != nil {
821+
return 0, err
822+
}
823+
return internal.ParseIntegerResponse(b)
824+
}

sugardb/api_generic_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,39 @@ func TestSugarDB_RANDOMKEY(t *testing.T) {
14101410

14111411
}
14121412

1413+
func TestSugarDB_Exists(t *testing.T) {
1414+
server := createSugarDB()
1415+
1416+
// Test with no keys
1417+
keys := []string{"key1", "key2", "key3"}
1418+
existsCount, err := server.Exists(keys...)
1419+
if err != nil {
1420+
t.Error(err)
1421+
return
1422+
}
1423+
if existsCount != 0 {
1424+
t.Errorf("EXISTS error, expected 0, got %d", existsCount)
1425+
}
1426+
1427+
// Test with some keys
1428+
for _, k := range keys {
1429+
err := presetValue(server, context.Background(), k, "")
1430+
if err != nil {
1431+
t.Error(err)
1432+
return
1433+
}
1434+
}
1435+
1436+
existsCount, err = server.Exists(keys...)
1437+
if err != nil {
1438+
t.Error(err)
1439+
return
1440+
}
1441+
if existsCount != len(keys) {
1442+
t.Errorf("EXISTS error, expected %d, got %d", len(keys), existsCount)
1443+
}
1444+
}
1445+
14131446
func TestSugarDB_DBSize(t *testing.T) {
14141447
server := createSugarDB()
14151448
got, err := server.DBSize()

0 commit comments

Comments
 (0)