Skip to content

Commit bd36cfa

Browse files
authored
Add command GeoAdd and the geospatial command interface (#3366)
* Add command GeoAdd and the geospatial command interface Signed-off-by: TJ Zhang <[email protected]>
1 parent 8de167d commit bd36cfa

9 files changed

+310
-6
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* Go: Adding support for Az Affinity ([#3235](https://github.com/valkey-io/valkey-glide/pull/3235))
1616
* Go: Adding support for advanced client configs and connectionTimeout ([#3290](https://github.com/valkey-io/valkey-glide/pull/3290))
1717
* Go: Add Cluster Scan support ([#3295](https://github.com/valkey-io/valkey-glide/pull/3295))
18+
* Go: Add `GeoAdd` and the Geospatial interface ([#3366](https://github.com/valkey-io/valkey-glide/pull/3366))
1819

1920
#### Breaking Changes
2021

go/api/base_client.go

+64
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type BaseClient interface {
4242
HyperLogLogCommands
4343
GenericBaseCommands
4444
BitmapCommands
45+
GeoSpatialCommands
4546
// Close terminates the client by closing all associated resources.
4647
Close()
4748
}
@@ -6465,3 +6466,66 @@ func (client *baseClient) ZLexCount(key string, rangeQuery *options.RangeByLex)
64656466
}
64666467
return handleIntResponse(result)
64676468
}
6469+
6470+
// Adds geospatial members with their positions to the specified sorted set stored at `key`.
6471+
// If a member is already a part of the sorted set, its position is updated.
6472+
//
6473+
// See [valkey.io] for details.
6474+
//
6475+
// Parameters:
6476+
//
6477+
// key - The key of the sorted set.
6478+
// membersToGeospatialData - A map of member names to their corresponding positions. See [options.GeospatialData].
6479+
// The command will report an error when index coordinates are out of the specified range.
6480+
//
6481+
// Return value:
6482+
//
6483+
// The number of elements added to the sorted set.
6484+
//
6485+
// [valkey.io]: https://valkey.io/commands/geoadd/
6486+
func (client *baseClient) GeoAdd(key string, membersToGeospatialData map[string]options.GeospatialData) (int64, error) {
6487+
result, err := client.executeCommand(
6488+
C.GeoAdd,
6489+
append([]string{key}, options.MapGeoDataToArray(membersToGeospatialData)...),
6490+
)
6491+
if err != nil {
6492+
return defaultIntResponse, err
6493+
}
6494+
return handleIntResponse(result)
6495+
}
6496+
6497+
// Adds geospatial members with their positions to the specified sorted set stored at `key`.
6498+
// If a member is already a part of the sorted set, its position is updated.
6499+
//
6500+
// See [valkey.io] for details.
6501+
//
6502+
// Parameters:
6503+
//
6504+
// key - The key of the sorted set.
6505+
// membersToGeospatialData - A map of member names to their corresponding positions. See [options.GeospatialData].
6506+
// The command will report an error when index coordinates are out of the specified range.
6507+
// geoAddOptions - The options for the GeoAdd command, see - [options.GeoAddOptions].
6508+
//
6509+
// Return value:
6510+
//
6511+
// The number of elements added to the sorted set.
6512+
//
6513+
// [valkey.io]: https://valkey.io/commands/geoadd/
6514+
func (client *baseClient) GeoAddWithOptions(
6515+
key string,
6516+
membersToGeospatialData map[string]options.GeospatialData,
6517+
geoAddOptions options.GeoAddOptions,
6518+
) (int64, error) {
6519+
args := []string{key}
6520+
optionsArgs, err := geoAddOptions.ToArgs()
6521+
if err != nil {
6522+
return defaultIntResponse, err
6523+
}
6524+
args = append(args, optionsArgs...)
6525+
args = append(args, options.MapGeoDataToArray(membersToGeospatialData)...)
6526+
result, err := client.executeCommand(C.GeoAdd, args)
6527+
if err != nil {
6528+
return defaultIntResponse, err
6529+
}
6530+
return handleIntResponse(result)
6531+
}

go/api/example_utils.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,10 @@ func getExampleGlideClusterClient() *GlideClusterClient {
8080
})
8181

8282
// Flush the database before each test to ensure a clean state.
83-
_, err := clusterClient.CustomCommandWithRoute([]string{"FLUSHALL"}, config.AllPrimaries) // todo: replace with client.FlushAll() when implemented
83+
_, err := clusterClient.CustomCommandWithRoute(
84+
[]string{"FLUSHALL"},
85+
config.AllPrimaries,
86+
) // todo: replace with client.FlushAll() when implemented
8487
if err != nil {
8588
fmt.Println("error flushing database: ", err)
8689
}

go/api/geospacial_commands.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
2+
3+
package api
4+
5+
import (
6+
"github.com/valkey-io/valkey-glide/go/api/options"
7+
)
8+
9+
// GeoSpatialCommands supports commands and transactions for the "Geo Spatial Commands" group
10+
// for standalone and cluster clients.
11+
//
12+
// See [valkey.io] for details.
13+
//
14+
// [valkey.io]: https://valkey.io/commands/#geo-spatial
15+
type GeoSpatialCommands interface {
16+
GeoAdd(key string, membersToGeospatialData map[string]options.GeospatialData) (int64, error)
17+
18+
GeoAddWithOptions(
19+
key string,
20+
membersToGeospatialData map[string]options.GeospatialData,
21+
options options.GeoAddOptions,
22+
) (int64, error)
23+
}

go/api/geospacial_commands_test.go

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
2+
3+
package api
4+
5+
import (
6+
"fmt"
7+
8+
"github.com/google/uuid"
9+
"github.com/valkey-io/valkey-glide/go/api/options"
10+
)
11+
12+
func ExampleGlideClient_GeoAdd() {
13+
client := getExampleGlideClient()
14+
15+
membersToCoordinates := map[string]options.GeospatialData{
16+
"Palermo": {Longitude: 13.361389, Latitude: 38.115556},
17+
"Catania": {Longitude: 15.087269, Latitude: 37.502669},
18+
}
19+
20+
result, err := client.GeoAdd(uuid.New().String(), membersToCoordinates)
21+
if err != nil {
22+
fmt.Println("Glide example failed with an error: ", err)
23+
}
24+
25+
fmt.Println(result)
26+
27+
// Output:
28+
// 2
29+
}
30+
31+
func ExampleGlideClusterClient_GeoAdd() {
32+
client := getExampleGlideClusterClient()
33+
34+
membersToCoordinates := map[string]options.GeospatialData{
35+
"Palermo": {Longitude: 13.361389, Latitude: 38.115556},
36+
"Catania": {Longitude: 15.087269, Latitude: 37.502669},
37+
}
38+
39+
result, err := client.GeoAdd(uuid.New().String(), membersToCoordinates)
40+
if err != nil {
41+
fmt.Println("Glide example failed with an error: ", err)
42+
}
43+
44+
fmt.Println(result)
45+
46+
// Output:
47+
// 2
48+
}

go/api/options/constants.go

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const (
2020
StoreKeyword string = "STORE"
2121
DbKeyword string = "DB"
2222
TypeKeyword string = "TYPE"
23+
ChangedKeyword string = "CH" // Valkey API keyword used to return total number of elements changed
24+
IncrKeyword string = "INCR" // Valkey API keyword to make zadd act like ZINCRBY.
2325
/// Valkey API keywords for stream commands
2426
IdleKeyword string = "IDLE" // ValKey API string to designate IDLE time in milliseconds
2527
TimeKeyword string = "TIME" // ValKey API string to designate TIME time in unix-milliseconds

go/api/options/geoadd_options.go

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
2+
3+
package options
4+
5+
import (
6+
"github.com/valkey-io/valkey-glide/go/utils"
7+
)
8+
9+
// Represents a geographic position defined by longitude and latitude
10+
// The exact limits, as specified by `EPSG:900913 / EPSG:3785 / OSGEO:41001` are:
11+
// - Longitude: -180 to 180 degrees
12+
// - Latitude: -85.05112878 to 85.05112878 degrees
13+
type GeospatialData struct {
14+
Latitude float64
15+
Longitude float64
16+
}
17+
18+
// Helper function to convert a geospatial members to geospatial data mapping to a slice of strings
19+
// The format is: latitude, longitude, member,...
20+
func MapGeoDataToArray(memberGeoMap map[string]GeospatialData) []string {
21+
result := make([]string, 0, len(memberGeoMap)*3)
22+
for member, geoData := range memberGeoMap {
23+
result = append(result, utils.FloatToString(geoData.Longitude), utils.FloatToString(geoData.Latitude), member)
24+
}
25+
return result
26+
}
27+
28+
// Optional arguments to `GeoAdd` in [GeoSpatialCommands]
29+
type GeoAddOptions struct {
30+
conditionalChange ConditionalSet
31+
changed bool
32+
}
33+
34+
func NewGeoAddOptions() *GeoAddOptions {
35+
return &GeoAddOptions{}
36+
}
37+
38+
// `conditionalChange` defines conditions for updating or adding elements with `ZADD` command.
39+
func (options *GeoAddOptions) SetConditionalChange(conditionalChange ConditionalSet) *GeoAddOptions {
40+
options.conditionalChange = conditionalChange
41+
return options
42+
}
43+
44+
// `Changed` changes the return value from the number of new elements added to the total number of elements changed.
45+
func (options *GeoAddOptions) SetChanged(changed bool) *GeoAddOptions {
46+
options.changed = changed
47+
return options
48+
}
49+
50+
// `ToArgs` converts the options to a list of arguments.
51+
func (opts *GeoAddOptions) ToArgs() ([]string, error) {
52+
args := []string{}
53+
var err error
54+
55+
if opts.conditionalChange == OnlyIfExists || opts.conditionalChange == OnlyIfDoesNotExist {
56+
args = append(args, string(opts.conditionalChange))
57+
}
58+
59+
if opts.changed {
60+
args = append(args, ChangedKeyword)
61+
}
62+
63+
return args, err
64+
}

go/api/options/zadd_options.go

-5
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,3 @@ const (
8888
// to "GT" in the Valkey API.
8989
ScoreGreaterThanCurrent UpdateOptions = "GT"
9090
)
91-
92-
const (
93-
ChangedKeyword string = "CH" // Valkey API keyword used to return total number of elements changed
94-
IncrKeyword string = "INCR" // Valkey API keyword to make zadd act like ZINCRBY.
95-
)

go/integTest/shared_commands_test.go

+104
Original file line numberDiff line numberDiff line change
@@ -9064,3 +9064,107 @@ func (suite *GlideTestSuite) TestZLexCount() {
90649064
assert.IsType(t, &errors.RequestError{}, err)
90659065
})
90669066
}
9067+
9068+
func (suite *GlideTestSuite) TestGeoAdd() {
9069+
suite.runWithDefaultClients(func(client api.BaseClient) {
9070+
t := suite.T()
9071+
key1 := "{testKey}:1-" + uuid.New().String()
9072+
key2 := "{testKey}:2-" + uuid.New().String()
9073+
9074+
// Test basic GEOADD
9075+
membersToCoordinates := map[string]options.GeospatialData{
9076+
"Palermo": {Longitude: 13.361389, Latitude: 38.115556},
9077+
"Catania": {Longitude: 15.087269, Latitude: 37.502669},
9078+
}
9079+
9080+
result, err := client.GeoAdd(key1, membersToCoordinates)
9081+
assert.NoError(t, err)
9082+
assert.Equal(t, int64(2), result)
9083+
9084+
// Test with NX option (only if not exists)
9085+
membersToCoordinates = map[string]options.GeospatialData{
9086+
"Catania": {Longitude: 15.087269, Latitude: 39},
9087+
}
9088+
result, err = client.GeoAddWithOptions(
9089+
key1,
9090+
membersToCoordinates,
9091+
*options.NewGeoAddOptions().SetConditionalChange(options.OnlyIfDoesNotExist),
9092+
)
9093+
assert.NoError(t, err)
9094+
assert.Equal(t, int64(0), result)
9095+
9096+
// Test with XX option (only if exists)
9097+
result, err = client.GeoAddWithOptions(
9098+
key1,
9099+
membersToCoordinates,
9100+
*options.NewGeoAddOptions().SetConditionalChange(options.OnlyIfExists),
9101+
)
9102+
assert.NoError(t, err)
9103+
assert.Equal(t, int64(0), result)
9104+
9105+
// Test with CH option (change coordinates)
9106+
membersToCoordinates = map[string]options.GeospatialData{
9107+
"Catania": {Longitude: 15.087269, Latitude: 40},
9108+
"Tel-Aviv": {Longitude: 32.0853, Latitude: 34.7818},
9109+
}
9110+
result, err = client.GeoAddWithOptions(
9111+
key1,
9112+
membersToCoordinates,
9113+
*options.NewGeoAddOptions().SetChanged(true),
9114+
)
9115+
assert.NoError(t, err)
9116+
assert.Equal(t, int64(2), result)
9117+
9118+
// Test error case with wrong key type
9119+
_, err = client.Set(key2, "bar")
9120+
assert.NoError(t, err)
9121+
9122+
_, err = client.GeoAddWithOptions(
9123+
key2,
9124+
membersToCoordinates,
9125+
*options.NewGeoAddOptions().SetChanged(true),
9126+
)
9127+
assert.Error(t, err)
9128+
assert.IsType(t, &errors.RequestError{}, err)
9129+
})
9130+
}
9131+
9132+
func (suite *GlideTestSuite) TestGeoAdd_InvalidArgs() {
9133+
suite.runWithDefaultClients(func(client api.BaseClient) {
9134+
t := suite.T()
9135+
key := "{testKey}:3-" + uuid.New().String()
9136+
9137+
// Test empty members
9138+
_, err := client.GeoAdd(key, map[string]options.GeospatialData{})
9139+
assert.Error(t, err)
9140+
assert.IsType(t, &errors.RequestError{}, err)
9141+
9142+
// Test invalid longitude (-181)
9143+
_, err = client.GeoAdd(key, map[string]options.GeospatialData{
9144+
"Place": {Longitude: -181, Latitude: 0},
9145+
})
9146+
assert.Error(t, err)
9147+
assert.IsType(t, &errors.RequestError{}, err)
9148+
9149+
// Test invalid longitude (181)
9150+
_, err = client.GeoAdd(key, map[string]options.GeospatialData{
9151+
"Place": {Longitude: 181, Latitude: 0},
9152+
})
9153+
assert.Error(t, err)
9154+
assert.IsType(t, &errors.RequestError{}, err)
9155+
9156+
// Test invalid latitude (86)
9157+
_, err = client.GeoAdd(key, map[string]options.GeospatialData{
9158+
"Place": {Longitude: 0, Latitude: 86},
9159+
})
9160+
assert.Error(t, err)
9161+
assert.IsType(t, &errors.RequestError{}, err)
9162+
9163+
// Test invalid latitude (-86)
9164+
_, err = client.GeoAdd(key, map[string]options.GeospatialData{
9165+
"Place": {Longitude: 0, Latitude: -86},
9166+
})
9167+
assert.Error(t, err)
9168+
assert.IsType(t, &errors.RequestError{}, err)
9169+
})
9170+
}

0 commit comments

Comments
 (0)