Skip to content

Commit

Permalink
Add EXIST and FEXIST command
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris Rice committed Mar 19, 2024
1 parent 72478a5 commit c17482a
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 4 deletions.
36 changes: 36 additions & 0 deletions core/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,42 @@
"since": "1.0.0",
"group": "keys"
},
"EXISTS": {
"summary": "Checks to see if a id exists",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
}
],
"since": "1.33.0",
"group": "keys"
},
"FEXISTS": {
"summary": "Checks to see if a field exists on a id",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"name": "field",
"type": "string"
}
],
"since": "1.33.0",
"group": "keys"
},
"PERSIST": {
"summary": "Remove the existing timeout on an id",
"complexity": "O(1)",
Expand Down
36 changes: 36 additions & 0 deletions core/commands_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,42 @@ var commandsJSON = `{
"since": "1.0.0",
"group": "keys"
},
"EXISTS": {
"summary": "Checks to see if a id exists",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
}
],
"since": "1.33.0",
"group": "keys"
},
"FEXISTS": {
"summary": "Checks to see if a field exists on a id",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"name": "field",
"type": "string"
}
],
"since": "1.33.0",
"group": "keys"
},
"PERSIST": {
"summary": "Remove the existing timeout on an id",
"complexity": "O(1)",
Expand Down
69 changes: 69 additions & 0 deletions internal/server/crud.go
Original file line number Diff line number Diff line change
Expand Up @@ -1080,3 +1080,72 @@ func (s *Server) cmdTTL(msg *Message) (resp.Value, error) {
}
return resp.IntegerValue(int(ttl)), nil
}

// EXISTS key id
func (s *Server) cmdEXISTS(msg *Message) (resp.Value, error) {
start := time.Now()

// >> Args

args := msg.Args
if len(args) != 3 {
return retrerr(errInvalidNumberOfArguments)
}
key, id := args[1], args[2]

// >> Operation

col, _ := s.cols.Get(key)
if col == nil {
return retrerr(errKeyNotFound)
}

o := col.Get(id)
exists := o != nil

// >> Response

if msg.OutputType == JSON {
return resp.SimpleStringValue(
`{"ok":true,"exists":` + strconv.FormatBool(exists) + `,"elapsed":"` +
time.Since(start).String() + "\"}"), nil
}
return resp.BoolValue(exists), nil
}

// FEXISTS key id field
func (s *Server) cmdFEXISTS(msg *Message) (resp.Value, error) {
start := time.Now()

// >> Args

args := msg.Args
if len(args) != 4 {
return retrerr(errInvalidNumberOfArguments)
}
key, id, field := args[1], args[2], args[3]

// >> Operation

col, _ := s.cols.Get(key)
if col == nil {
return retrerr(errKeyNotFound)
}

o := col.Get(id)
if o == nil {
return retrerr(errIDNotFound)
}

f := o.Fields().Get(field)
exists := f.Name() != ""

// >> Response

if msg.OutputType == JSON {
return resp.SimpleStringValue(
`{"ok":true,"exists":` + strconv.FormatBool(exists) + `,"elapsed":"` +
time.Since(start).String() + "\"}"), nil
}
return resp.BoolValue(exists), nil
}
10 changes: 7 additions & 3 deletions internal/server/scripts.go
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,10 @@ func (s *Server) commandInScript(msg *Message) (
res, err = s.cmdTYPE(msg)
case "keys":
res, err = s.cmdKEYS(msg)
case "exists":
res, err = s.cmdEXISTS(msg)
case "fexists":
res, err = s.cmdFEXISTS(msg)
case "test":
res, err = s.cmdTEST(msg)
case "server":
Expand Down Expand Up @@ -735,7 +739,7 @@ func (s *Server) luaTile38AtomicRW(msg *Message) (resp.Value, error) {
return resp.NullValue(), errReadOnly
}
case "get", "keys", "scan", "nearby", "within", "intersects", "hooks", "search",
"ttl", "bounds", "server", "info", "type", "jget", "test":
"ttl", "bounds", "server", "info", "type", "jget", "exists", "fexists", "test":
// read operations
if s.config.followHost() != "" && !s.fcuponce {
return resp.NullValue(), errCatchingUp
Expand Down Expand Up @@ -788,7 +792,7 @@ func (s *Server) luaTile38AtomicRO(msg *Message) (resp.Value, error) {
return resp.NullValue(), errReadOnly

case "get", "keys", "scan", "nearby", "within", "intersects", "hooks", "search",
"ttl", "bounds", "server", "info", "type", "jget", "test":
"ttl", "bounds", "server", "info", "type", "jget", "exists", "fexists", "test":
// read operations
if s.config.followHost() != "" && !s.fcuponce {
return resp.NullValue(), errCatchingUp
Expand Down Expand Up @@ -839,7 +843,7 @@ func (s *Server) luaTile38NonAtomic(msg *Message) (resp.Value, error) {
return resp.NullValue(), errReadOnly
}
case "get", "keys", "scan", "nearby", "within", "intersects", "hooks", "search",
"ttl", "bounds", "server", "info", "type", "jget", "test":
"ttl", "bounds", "server", "info", "type", "jget", "exists", "fexists", "test":
// read operations
s.mu.RLock()
defer s.mu.RUnlock()
Expand Down
6 changes: 5 additions & 1 deletion internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1024,7 +1024,7 @@ func (s *Server) handleInputCommand(client *Client, msg *Message) error {
}
case "get", "keys", "scan", "nearby", "within", "intersects", "hooks",
"chans", "search", "ttl", "bounds", "server", "info", "type", "jget",
"evalro", "evalrosha", "healthz", "role":
"evalro", "evalrosha", "healthz", "role", "exists", "fexists":
// read operations

s.mu.RLock()
Expand Down Expand Up @@ -1237,6 +1237,10 @@ func (s *Server) command(msg *Message, client *Client) (
res, err = s.cmdTYPE(msg)
case "keys":
res, err = s.cmdKEYS(msg)
case "exists":
res, err = s.cmdEXISTS(msg)
case "fexists":
res, err = s.cmdFEXISTS(msg)
case "output":
res, err = s.cmdOUTPUT(msg)
case "aof":
Expand Down
25 changes: 25 additions & 0 deletions tests/keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ func subTestKeys(g *testGroup) {
g.regSubTest("SET", keys_SET_test)
g.regSubTest("STATS", keys_STATS_test)
g.regSubTest("TTL", keys_TTL_test)
g.regSubTest("EXIST", keys_EXISTS_test)
g.regSubTest("FEXIST", keys_FEXISTS_test)
g.regSubTest("SET EX", keys_SET_EX_test)
g.regSubTest("PDEL", keys_PDEL_test)
g.regSubTest("FIELDS", keys_FIELDS_test)
Expand Down Expand Up @@ -409,6 +411,29 @@ func keys_TTL_test(mc *mockServer) error {
Do("TTL", "mykey", "myid2").JSON().Err("id not found"),
)
}
func keys_EXISTS_test(mc *mockServer) error {
return mc.DoBatch(
Do("SET", "mykey", "myid", "STRING", "value").OK(),
Do("EXISTS", "mykey", "myid").Str("1"),
Do("EXISTS", "mykey", "myid").JSON().Str(`{"ok":true,"exists":true}`),
Do("EXISTS", "mykey", "myid2").Str("0"),
Do("EXISTS", "mykey", "myid2").JSON().Str(`{"ok":true,"exists":false}`),
Do("EXISTS", "mykey").Err("wrong number of arguments for 'exists' command"),
Do("EXISTS", "mykey2", "myid").JSON().Err("key not found"),
)
}
func keys_FEXISTS_test(mc *mockServer) error {
return mc.DoBatch(
Do("SET", "mykey", "myid", "FIELD", "f1", "123", "STRING", "value").OK(),
Do("FEXISTS", "mykey", "myid", "f1").Str("1"),
Do("FEXISTS", "mykey", "myid", "f1").JSON().Str(`{"ok":true,"exists":true}`),
Do("FEXISTS", "mykey", "myid", "f2").Str("0"),
Do("FEXISTS", "mykey", "myid", "f2").JSON().Str(`{"ok":true,"exists":false}`),
Do("FEXISTS", "mykey", "myid").Err("wrong number of arguments for 'fexists' command"),
Do("FEXISTS", "mykey2", "myid", "f2").JSON().Err("key not found"),
Do("FEXISTS", "mykey", "myid2", "f2").JSON().Err("id not found"),
)
}

func keys_SET_EX_test(mc *mockServer) (err error) {
rand.Seed(time.Now().UnixNano())
Expand Down

0 comments on commit c17482a

Please sign in to comment.