diff --git a/core/commands.json b/core/commands.json index aa11fefc..75c53d17 100644 --- a/core/commands.json +++ b/core/commands.json @@ -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)", diff --git a/core/commands_gen.go b/core/commands_gen.go index 2bf55886..ba641db8 100644 --- a/core/commands_gen.go +++ b/core/commands_gen.go @@ -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)", diff --git a/internal/server/crud.go b/internal/server/crud.go index 18d197b7..a5e20708 100644 --- a/internal/server/crud.go +++ b/internal/server/crud.go @@ -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 +} diff --git a/internal/server/scripts.go b/internal/server/scripts.go index d8abb3c5..4b89b73a 100644 --- a/internal/server/scripts.go +++ b/internal/server/scripts.go @@ -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": @@ -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 @@ -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 @@ -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() diff --git a/internal/server/server.go b/internal/server/server.go index 1736f098..6b553745 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -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() @@ -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": diff --git a/tests/keys_test.go b/tests/keys_test.go index 7e4209c0..f77a5a8d 100644 --- a/tests/keys_test.go +++ b/tests/keys_test.go @@ -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) @@ -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())