diff --git a/README.md b/README.md index 997d580f..09be8e3f 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,11 @@ To set a field when an object already exists: > fset fleet truck1 speed 90 ``` +To get a field when an object already exists: +``` +> fget fleet truck1 speed +``` + ## Searching Tile38 has support to search for objects and points that are within or intersects other objects. All object types can be searched including Polygons, MultiPolygons, GeometryCollections, etc. diff --git a/core/commands.json b/core/commands.json index 75c53d17..d4e5ff90 100644 --- a/core/commands.json +++ b/core/commands.json @@ -232,6 +232,26 @@ "since": "1.0.0", "group": "keys" }, + "FGET": { + "summary": "Gets the value for the field of an id", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "string" + }, + { + "name": "id", + "type": "string" + }, + { + "name": "field", + "type": "string" + } + ], + "since": "1.33.0", + "group": "keys" + }, "BOUNDS": { "summary": "Get the combined bounds of all the objects in a key", "complexity": "O(1)", diff --git a/core/commands_gen.go b/core/commands_gen.go index ba641db8..4a233d5e 100644 --- a/core/commands_gen.go +++ b/core/commands_gen.go @@ -398,6 +398,26 @@ var commandsJSON = `{ "since": "1.0.0", "group": "keys" }, + "FGET": { + "summary": "Gets the value for the field of an id", + "complexity": "O(1)", + "arguments": [ + { + "name": "key", + "type": "string" + }, + { + "name": "id", + "type": "string" + }, + { + "name": "field", + "type": "string" + } + ], + "since": "1.33.0", + "group": "keys" + }, "BOUNDS": { "summary": "Get the combined bounds of all the objects in a key", "complexity": "O(1)", diff --git a/go.mod b/go.mod index 4965e5cc..167e3a80 100644 --- a/go.mod +++ b/go.mod @@ -112,5 +112,5 @@ require ( google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect ) diff --git a/go.sum b/go.sum index 7c927dcf..cbf5ccb1 100644 --- a/go.sum +++ b/go.sum @@ -732,8 +732,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/server/crud.go b/internal/server/crud.go index a5e20708..ee885830 100644 --- a/internal/server/crud.go +++ b/internal/server/crud.go @@ -907,6 +907,46 @@ func (s *Server) cmdFSET(msg *Message) (resp.Value, commandDetails, error) { return res, d, nil } +// FGET key id field +func (s *Server) cmdFGET(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) + + // >> Response + + var buf bytes.Buffer + switch msg.OutputType { + case JSON: + buf.WriteString(`{"ok":true`) + buf.WriteString(`,"value":` + f.Value().JSON()) + buf.WriteString(`,"elapsed":"` + time.Since(start).String() + "\"}") + return resp.StringValue(buf.String()), nil + case RESP: + return resp.StringValue(f.Value().Data()), nil + } + return NOMessage, nil +} + // EXPIRE key id seconds func (s *Server) cmdEXPIRE(msg *Message) (resp.Value, commandDetails, error) { start := time.Now() diff --git a/internal/server/scripts.go b/internal/server/scripts.go index 4b89b73a..e5320ad9 100644 --- a/internal/server/scripts.go +++ b/internal/server/scripts.go @@ -665,6 +665,8 @@ func (s *Server) commandInScript(msg *Message) ( res, err = s.cmdBOUNDS(msg) case "get": res, err = s.cmdGET(msg) + case "fget": + res, err = s.cmdFGET(msg) case "jget": res, err = s.cmdJget(msg) case "jset": @@ -739,7 +741,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", "exists", "fexists", "test": + "ttl", "bounds", "server", "info", "type", "jget", "fget", "exists", "fexists", "test": // read operations if s.config.followHost() != "" && !s.fcuponce { return resp.NullValue(), errCatchingUp @@ -792,7 +794,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", "exists", "fexists", "test": + "ttl", "bounds", "server", "info", "type", "jget", "fget", "exists", "fexists", "test": // read operations if s.config.followHost() != "" && !s.fcuponce { return resp.NullValue(), errCatchingUp @@ -843,7 +845,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", "exists", "fexists", "test": + "ttl", "bounds", "server", "info", "type", "jget", "fget", "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 6b553745..26c4e5fd 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", "exists", "fexists": + "evalro", "evalrosha", "healthz", "role", "fget", "exists", "fexists": // read operations s.mu.RLock() @@ -1227,6 +1227,8 @@ func (s *Server) command(msg *Message, client *Client) ( res, err = s.cmdBOUNDS(msg) case "get": res, err = s.cmdGET(msg) + case "fget": + res, err = s.cmdFGET(msg) case "jget": res, err = s.cmdJget(msg) case "jset": diff --git a/tests/keys_test.go b/tests/keys_test.go index f77a5a8d..8063ad4b 100644 --- a/tests/keys_test.go +++ b/tests/keys_test.go @@ -19,6 +19,7 @@ func subTestKeys(g *testGroup) { g.regSubTest("RENAMENX", keys_RENAMENX_test) g.regSubTest("EXPIRE", keys_EXPIRE_test) g.regSubTest("FSET", keys_FSET_test) + g.regSubTest("FGET", keys_FGET_test) g.regSubTest("GET", keys_GET_test) g.regSubTest("KEYS", keys_KEYS_test) g.regSubTest("PERSIST", keys_PERSIST_test) @@ -203,6 +204,25 @@ func keys_FSET_test(mc *mockServer) error { Do("FSET", "mykey", "myid", "f2", 0).JSON().OK(), ) } +func keys_FGET_test(mc *mockServer) error { + return mc.DoBatch( + Do("SET", "mykey", "myid", "HASH", "9my5xp7").OK(), + Do("GET", "mykey", "myid", "WITHFIELDS", "HASH", 7).Str("[9my5xp7]"), + Do("FSET", "mykey", "myid", "f1", 105.6).Str("1"), + Do("FGET", "mykey", "myid", "f1").Str("105.6"), + Do("FSET", "mykey", "myid", "f1", 1.1, "f2", 2.2).Str("2"), + Do("FGET", "mykey", "myid", "f2").Str("2.2"), + Do("FGET", "mykey", "myid", "f1").Str("1.1"), + Do("FGET", "mykey", "myid", "f1").JSON().Str(`{"ok":true,"value":1.1}`), + Do("FSET", "mykey", "myid", "f3", "a").Str("1"), + Do("FGET", "mykey", "myid", "f3").Str("a"), + Do("FGET", "mykey", "myid", "f4").Str("0"), + Do("FGET", "mykey", "myid", "f4").JSON().Str(`{"ok":true,"value":0}`), + Do("FGET", "mykey", "myid").Err("wrong number of arguments for 'fget' command"), + Do("FGET", "mykey2", "myid", "a", "b").Err("key not found"), + Do("FGET", "mykey", "myid2", "a", "b").Err("id not found"), + ) +} func keys_GET_test(mc *mockServer) error { return mc.DoBatch( Do("SET", "mykey", "myid", "STRING", "value").OK(),