From 2d616c706d6a5c5682ca936dcf4aaf2563c75b26 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Wed, 8 Oct 2025 09:44:15 -0700 Subject: [PATCH 01/27] GeoAdd, GeoDist Signed-off-by: Alex Rehnby-Martin --- .../BaseClient.GeospatialCommands.cs | 36 ++++++++ .../Commands/IGeospatialCommands.cs | 92 +++++++++++++++++++ .../Internals/Request.GeospatialCommands.cs | 54 +++++++++++ .../Pipeline/BaseBatch.GeospatialCommands.cs | 29 ++++++ sources/Valkey.Glide/Pipeline/IBatch.cs | 2 +- .../Pipeline/IBatchGeospatialCommands.cs | 24 +++++ .../GeospatialCommandTests.cs | 92 +++++++++++++++++++ 7 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 sources/Valkey.Glide/BaseClient.GeospatialCommands.cs create mode 100644 sources/Valkey.Glide/Commands/IGeospatialCommands.cs create mode 100644 sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs create mode 100644 sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs create mode 100644 sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs create mode 100644 tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs diff --git a/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs b/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs new file mode 100644 index 00000000..96954f08 --- /dev/null +++ b/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs @@ -0,0 +1,36 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +using Valkey.Glide.Commands; +using Valkey.Glide.Internals; + +namespace Valkey.Glide; + +public abstract partial class BaseClient : IGeospatialCommands +{ + /// + public async Task GeoAddAsync(ValkeyKey key, double longitude, double latitude, ValkeyValue member, CommandFlags flags = CommandFlags.None) + { + return await GeoAddAsync(key, new GeoEntry(longitude, latitude, member), flags).ConfigureAwait(false); + } + + /// + public async Task GeoAddAsync(ValkeyKey key, GeoEntry value, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.GeoAddAsync(key, value)); + } + + /// + public async Task GeoAddAsync(ValkeyKey key, GeoEntry[] values, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.GeoAddAsync(key, values)); + } + + /// + public async Task GeoDistanceAsync(ValkeyKey key, ValkeyValue member1, ValkeyValue member2, GeoUnit unit = GeoUnit.Meters, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.GeoDistanceAsync(key, member1, member2, unit)); + } +} \ No newline at end of file diff --git a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs new file mode 100644 index 00000000..a5454769 --- /dev/null +++ b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs @@ -0,0 +1,92 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +namespace Valkey.Glide.Commands; + +/// +/// Supports commands for the "Geospatial Commands" group for standalone and cluster clients. +///
+/// See more on valkey.io. +///
+public interface IGeospatialCommands +{ + /// + /// Adds the specified geospatial items (longitude, latitude, name) to the specified key. + /// If a member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. + /// + /// + /// The key of the sorted set. + /// The longitude coordinate according to WGS84. + /// The latitude coordinate according to WGS84. + /// The name of the member to add. + /// The flags to use for this operation. Currently flags are ignored. + /// if the member was added. if the member was already a member of the sorted set and the score was updated. + /// + /// + /// + /// bool wasAdded = await client.GeoAddAsync("mygeo", 13.361389, 38.115556, "Palermo"); + /// + /// + /// + Task GeoAddAsync(ValkeyKey key, double longitude, double latitude, ValkeyValue member, CommandFlags flags = CommandFlags.None); + + /// + /// Adds the specified geospatial item to the specified key. + /// If a member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. + /// + /// + /// The key of the sorted set. + /// The geospatial item to add. + /// The flags to use for this operation. Currently flags are ignored. + /// if the member was added. if the member was already a member of the sorted set and the score was updated. + /// + /// + /// + /// var entry = new GeoEntry(13.361389, 38.115556, "Palermo"); + /// bool wasAdded = await client.GeoAddAsync("mygeo", entry); + /// + /// + /// + Task GeoAddAsync(ValkeyKey key, GeoEntry value, CommandFlags flags = CommandFlags.None); + + /// + /// Adds the specified geospatial items to the specified key. + /// If a member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. + /// + /// + /// The key of the sorted set. + /// The geospatial items to add. + /// The flags to use for this operation. Currently flags are ignored. + /// The number of elements added to the sorted set, not including elements already existing for which the score was updated. + /// + /// + /// + /// var entries = new GeoEntry[] + /// { + /// new GeoEntry(13.361389, 38.115556, "Palermo"), + /// new GeoEntry(15.087269, 37.502669, "Catania") + /// }; + /// long addedCount = await client.GeoAddAsync("mygeo", entries); + /// + /// + /// + Task GeoAddAsync(ValkeyKey key, GeoEntry[] values, CommandFlags flags = CommandFlags.None); + + /// + /// Returns the distance between two members in the geospatial index represented by the sorted set. + /// + /// + /// The key of the sorted set. + /// The first member. + /// The second member. + /// The unit of distance (defaults to meters). + /// The flags to use for this operation. Currently flags are ignored. + /// The distance between the two members in the specified unit. Returns if one or both members are missing. + /// + /// + /// + /// double? distance = await client.GeoDistanceAsync("mygeo", "Palermo", "Catania", GeoUnit.Kilometers); + /// + /// + /// + Task GeoDistanceAsync(ValkeyKey key, ValkeyValue member1, ValkeyValue member2, GeoUnit unit = GeoUnit.Meters, CommandFlags flags = CommandFlags.None); +} \ No newline at end of file diff --git a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs new file mode 100644 index 00000000..c364e274 --- /dev/null +++ b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs @@ -0,0 +1,54 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +using static Valkey.Glide.Internals.FFI; + +namespace Valkey.Glide.Internals; + +internal static partial class Request +{ + /// + /// Creates a request for GEOADD command. + /// + /// The key of the sorted set. + /// The geospatial item to add. + /// A with the request. + public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry value) + { + GlideString[] args = [key.ToGlideString(), value.Longitude.ToGlideString(), value.Latitude.ToGlideString(), value.Member.ToGlideString()]; + return Boolean(RequestType.GeoAdd, args); + } + + /// + /// Creates a request for GEOADD command with multiple values. + /// + /// The key of the sorted set. + /// The geospatial items to add. + /// A with the request. + public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values) + { + List args = [key.ToGlideString()]; + + foreach (var value in values) + { + args.Add(value.Longitude.ToGlideString()); + args.Add(value.Latitude.ToGlideString()); + args.Add(value.Member.ToGlideString()); + } + + return Simple(RequestType.GeoAdd, [.. args]); + } + + /// + /// Creates a request for GEODIST command. + /// + /// The key of the sorted set. + /// The first member. + /// The second member. + /// The unit of distance. + /// A with the request. + public static Cmd GeoDistanceAsync(ValkeyKey key, ValkeyValue member1, ValkeyValue member2, GeoUnit unit) + { + GlideString[] args = [key.ToGlideString(), member1.ToGlideString(), member2.ToGlideString(), unit.ToLiteral()]; + return Simple(RequestType.GeoDist, args, true); + } +} \ No newline at end of file diff --git a/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs b/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs new file mode 100644 index 00000000..77a89ca5 --- /dev/null +++ b/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs @@ -0,0 +1,29 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +using static Valkey.Glide.Internals.Request; + +namespace Valkey.Glide.Pipeline; + +/// +/// Geospatial commands for BaseBatch. +/// +public abstract partial class BaseBatch +{ + /// + public T GeoAdd(ValkeyKey key, double longitude, double latitude, ValkeyValue member) => GeoAdd(key, new GeoEntry(longitude, latitude, member)); + + /// + public T GeoAdd(ValkeyKey key, GeoEntry value) => AddCmd(GeoAddAsync(key, value)); + + /// + public T GeoAdd(ValkeyKey key, GeoEntry[] values) => AddCmd(GeoAddAsync(key, values)); + + /// + public T GeoDistance(ValkeyKey key, ValkeyValue member1, ValkeyValue member2, GeoUnit unit = GeoUnit.Meters) => AddCmd(GeoDistanceAsync(key, member1, member2, unit)); + + // Explicit interface implementations for IBatchGeospatialCommands + IBatch IBatchGeospatialCommands.GeoAdd(ValkeyKey key, double longitude, double latitude, ValkeyValue member) => GeoAdd(key, longitude, latitude, member); + IBatch IBatchGeospatialCommands.GeoAdd(ValkeyKey key, GeoEntry value) => GeoAdd(key, value); + IBatch IBatchGeospatialCommands.GeoAdd(ValkeyKey key, GeoEntry[] values) => GeoAdd(key, values); + IBatch IBatchGeospatialCommands.GeoDistance(ValkeyKey key, ValkeyValue member1, ValkeyValue member2, GeoUnit unit) => GeoDistance(key, member1, member2, unit); +} \ No newline at end of file diff --git a/sources/Valkey.Glide/Pipeline/IBatch.cs b/sources/Valkey.Glide/Pipeline/IBatch.cs index 60843655..1dadd806 100644 --- a/sources/Valkey.Glide/Pipeline/IBatch.cs +++ b/sources/Valkey.Glide/Pipeline/IBatch.cs @@ -7,7 +7,7 @@ namespace Valkey.Glide.Pipeline; // BaseBatch was split into two types, one for docs, another for the impl. This also ease the testing. -internal interface IBatch : IBatchSetCommands, IBatchStringCommands, IBatchListCommands, IBatchSortedSetCommands, IBatchGenericCommands, IBatchConnectionManagementCommands, IBatchHashCommands, IBatchServerManagementCommands +internal interface IBatch : IBatchSetCommands, IBatchStringCommands, IBatchListCommands, IBatchSortedSetCommands, IBatchGenericCommands, IBatchConnectionManagementCommands, IBatchHashCommands, IBatchServerManagementCommands, IBatchGeospatialCommands { // inherit all docs except `remarks` section which stores en example (not relevant for batch) // and returns section, because we customize it. diff --git a/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs b/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs new file mode 100644 index 00000000..27f908e1 --- /dev/null +++ b/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs @@ -0,0 +1,24 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +using Valkey.Glide.Commands; + +namespace Valkey.Glide.Pipeline; + +internal interface IBatchGeospatialCommands +{ + /// + /// Command Response - + IBatch GeoAdd(ValkeyKey key, double longitude, double latitude, ValkeyValue member); + + /// + /// Command Response - + IBatch GeoAdd(ValkeyKey key, GeoEntry value); + + /// + /// Command Response - + IBatch GeoAdd(ValkeyKey key, GeoEntry[] values); + + /// + /// Command Response - + IBatch GeoDistance(ValkeyKey key, ValkeyValue member1, ValkeyValue member2, GeoUnit unit = GeoUnit.Meters); +} \ No newline at end of file diff --git a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs new file mode 100644 index 00000000..f1904cbf --- /dev/null +++ b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs @@ -0,0 +1,92 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +namespace Valkey.Glide.IntegrationTests; + +public class GeospatialCommandTests(TestConfiguration config) +{ + public TestConfiguration Config { get; } = config; + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoAdd_SingleEntry_ReturnsTrue(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + double longitude = 13.361389; + double latitude = 38.115556; + string member = "Palermo"; + + bool result = await client.GeoAddAsync(key, longitude, latitude, member); + Assert.True(result); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoAdd_GeoEntry_ReturnsTrue(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + var entry = new GeoEntry(13.361389, 38.115556, "Palermo"); + + bool result = await client.GeoAddAsync(key, entry); + Assert.True(result); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoAdd_MultipleEntries_ReturnsCount(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + + long result = await client.GeoAddAsync(key, entries); + Assert.Equal(2, result); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoAdd_DuplicateEntry_ReturnsFalse(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + double longitude = 13.361389; + double latitude = 38.115556; + string member = "Palermo"; + + bool firstResult = await client.GeoAddAsync(key, longitude, latitude, member); + bool secondResult = await client.GeoAddAsync(key, longitude, latitude, member); + + Assert.True(firstResult); + Assert.False(secondResult); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoDistance_ReturnsCorrectDistance(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + + await client.GeoAddAsync(key, entries); + + double? distance = await client.GeoDistanceAsync(key, "Palermo", "Catania", GeoUnit.Kilometers); + Assert.NotNull(distance); + Assert.True(distance > 160 && distance < 170); // Approximate distance between Palermo and Catania + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoDistance_NonExistentMember_ReturnsNull(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo"); + + double? distance = await client.GeoDistanceAsync(key, "Palermo", "NonExistent"); + Assert.Null(distance); + } +} \ No newline at end of file From e64573bc020046b036216e595e27deca7f23a3cd Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Wed, 8 Oct 2025 10:09:37 -0700 Subject: [PATCH 02/27] rest of geo commands except search Signed-off-by: Alex Rehnby-Martin --- .../BaseClient.GeospatialCommands.cs | 28 ++++++ .../Commands/IGeospatialCommands.cs | 68 ++++++++++++++ .../Internals/Request.GeospatialCommands.cs | 63 +++++++++++++ .../Pipeline/BaseBatch.GeospatialCommands.cs | 16 ++++ .../Pipeline/IBatchGeospatialCommands.cs | 16 ++++ .../GeospatialCommandTests.cs | 91 +++++++++++++++++++ 6 files changed, 282 insertions(+) diff --git a/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs b/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs index 96954f08..d0f5d8d7 100644 --- a/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs +++ b/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs @@ -33,4 +33,32 @@ public async Task GeoAddAsync(ValkeyKey key, GeoEntry[] values, CommandFla Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); return await Command(Request.GeoDistanceAsync(key, member1, member2, unit)); } + + /// + public async Task GeoHashAsync(ValkeyKey key, ValkeyValue member, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.GeoHashAsync(key, member)); + } + + /// + public async Task GeoHashAsync(ValkeyKey key, ValkeyValue[] members, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.GeoHashAsync(key, members)); + } + + /// + public async Task GeoPositionAsync(ValkeyKey key, ValkeyValue member, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.GeoPositionAsync(key, member)); + } + + /// + public async Task GeoPositionAsync(ValkeyKey key, ValkeyValue[] members, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.GeoPositionAsync(key, members)); + } } \ No newline at end of file diff --git a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs index a5454769..1eb87bca 100644 --- a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs +++ b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs @@ -89,4 +89,72 @@ public interface IGeospatialCommands /// /// Task GeoDistanceAsync(ValkeyKey key, ValkeyValue member1, ValkeyValue member2, GeoUnit unit = GeoUnit.Meters, CommandFlags flags = CommandFlags.None); + + /// + /// Returns the geohash string for a single member in the geospatial index represented by the sorted set. + /// + /// + /// The key of the sorted set. + /// The member to get the geohash for. + /// The flags to use for this operation. Currently flags are ignored. + /// The geohash string for the member. Returns if the member is missing. + /// + /// + /// + /// string? hash = await client.GeoHashAsync("mygeo", "Palermo"); + /// + /// + /// + Task GeoHashAsync(ValkeyKey key, ValkeyValue member, CommandFlags flags = CommandFlags.None); + + /// + /// Returns the geohash strings for multiple members in the geospatial index represented by the sorted set. + /// + /// + /// The key of the sorted set. + /// The members to get the geohashes for. + /// The flags to use for this operation. Currently flags are ignored. + /// An array of geohash strings for the members. Returns for missing members. + /// + /// + /// + /// string?[] hashes = await client.GeoHashAsync("mygeo", new ValkeyValue[] { "Palermo", "Catania" }); + /// + /// + /// + Task GeoHashAsync(ValkeyKey key, ValkeyValue[] members, CommandFlags flags = CommandFlags.None); + + /// + /// Returns the longitude and latitude for a single member in the geospatial index represented by the sorted set. + /// + /// + /// The key of the sorted set. + /// The member to get the position for. + /// The flags to use for this operation. Currently flags are ignored. + /// The longitude and latitude for the member. Returns if the member is missing. + /// + /// + /// + /// GeoPosition? position = await client.GeoPositionAsync("mygeo", "Palermo"); + /// + /// + /// + Task GeoPositionAsync(ValkeyKey key, ValkeyValue member, CommandFlags flags = CommandFlags.None); + + /// + /// Returns the longitude and latitude for multiple members in the geospatial index represented by the sorted set. + /// + /// + /// The key of the sorted set. + /// The members to get the positions for. + /// The flags to use for this operation. Currently flags are ignored. + /// An array of longitude and latitude for the members. Returns for missing members. + /// + /// + /// + /// GeoPosition?[] positions = await client.GeoPositionAsync("mygeo", new ValkeyValue[] { "Palermo", "Catania" }); + /// + /// + /// + Task GeoPositionAsync(ValkeyKey key, ValkeyValue[] members, CommandFlags flags = CommandFlags.None); } \ No newline at end of file diff --git a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs index c364e274..54573183 100644 --- a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs @@ -51,4 +51,67 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values) GlideString[] args = [key.ToGlideString(), member1.ToGlideString(), member2.ToGlideString(), unit.ToLiteral()]; return Simple(RequestType.GeoDist, args, true); } + + /// + /// Creates a request for GEOHASH command for a single member. + /// + /// The key of the sorted set. + /// The member to get the geohash for. + /// A with the request. + public static Cmd GeoHashAsync(ValkeyKey key, ValkeyValue member) + { + GlideString[] args = [key.ToGlideString(), member.ToGlideString()]; + return new(RequestType.GeoHash, args, false, response => response.Length > 0 ? response[0]?.ToString() : null); + } + + /// + /// Creates a request for GEOHASH command for multiple members. + /// + /// The key of the sorted set. + /// The members to get the geohashes for. + /// A with the request. + public static Cmd GeoHashAsync(ValkeyKey key, ValkeyValue[] members) + { + GlideString[] args = [key.ToGlideString(), .. members.Select(m => m.ToGlideString())]; + return new(RequestType.GeoHash, args, false, response => response.Select(item => item?.ToString()).ToArray()); + } + + /// + /// Creates a request for GEOPOS command for a single member. + /// + /// The key of the sorted set. + /// The member to get the position for. + /// A with the request. + public static Cmd GeoPositionAsync(ValkeyKey key, ValkeyValue member) + { + GlideString[] args = [key.ToGlideString(), member.ToGlideString()]; + return new(RequestType.GeoPos, args, false, response => + { + if (response.Length == 0 || response[0] == null) return (GeoPosition?)null; + var posArray = (object[])response[0]; + if (posArray.Length < 2 || posArray[0] == null || posArray[1] == null) return (GeoPosition?)null; + return (GeoPosition?)new GeoPosition(double.Parse(posArray[0].ToString()!), double.Parse(posArray[1].ToString()!)); + }); + } + + /// + /// Creates a request for GEOPOS command for multiple members. + /// + /// The key of the sorted set. + /// The members to get the positions for. + /// A with the request. + public static Cmd GeoPositionAsync(ValkeyKey key, ValkeyValue[] members) + { + GlideString[] args = [key.ToGlideString(), .. members.Select(m => m.ToGlideString())]; + return new(RequestType.GeoPos, args, false, response => + { + return response.Select(item => + { + if (item == null) return (GeoPosition?)null; + var posArray = (object[])item; + if (posArray.Length < 2 || posArray[0] == null || posArray[1] == null) return (GeoPosition?)null; + return (GeoPosition?)new GeoPosition(double.Parse(posArray[0].ToString()!), double.Parse(posArray[1].ToString()!)); + }).ToArray(); + }); + } } \ No newline at end of file diff --git a/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs b/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs index 77a89ca5..3a304035 100644 --- a/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs +++ b/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs @@ -21,9 +21,25 @@ public abstract partial class BaseBatch /// public T GeoDistance(ValkeyKey key, ValkeyValue member1, ValkeyValue member2, GeoUnit unit = GeoUnit.Meters) => AddCmd(GeoDistanceAsync(key, member1, member2, unit)); + /// + public T GeoHash(ValkeyKey key, ValkeyValue member) => AddCmd(GeoHashAsync(key, member)); + + /// + public T GeoHash(ValkeyKey key, ValkeyValue[] members) => AddCmd(GeoHashAsync(key, members)); + + /// + public T GeoPosition(ValkeyKey key, ValkeyValue member) => AddCmd(GeoPositionAsync(key, member)); + + /// + public T GeoPosition(ValkeyKey key, ValkeyValue[] members) => AddCmd(GeoPositionAsync(key, members)); + // Explicit interface implementations for IBatchGeospatialCommands IBatch IBatchGeospatialCommands.GeoAdd(ValkeyKey key, double longitude, double latitude, ValkeyValue member) => GeoAdd(key, longitude, latitude, member); IBatch IBatchGeospatialCommands.GeoAdd(ValkeyKey key, GeoEntry value) => GeoAdd(key, value); IBatch IBatchGeospatialCommands.GeoAdd(ValkeyKey key, GeoEntry[] values) => GeoAdd(key, values); IBatch IBatchGeospatialCommands.GeoDistance(ValkeyKey key, ValkeyValue member1, ValkeyValue member2, GeoUnit unit) => GeoDistance(key, member1, member2, unit); + IBatch IBatchGeospatialCommands.GeoHash(ValkeyKey key, ValkeyValue member) => GeoHash(key, member); + IBatch IBatchGeospatialCommands.GeoHash(ValkeyKey key, ValkeyValue[] members) => GeoHash(key, members); + IBatch IBatchGeospatialCommands.GeoPosition(ValkeyKey key, ValkeyValue member) => GeoPosition(key, member); + IBatch IBatchGeospatialCommands.GeoPosition(ValkeyKey key, ValkeyValue[] members) => GeoPosition(key, members); } \ No newline at end of file diff --git a/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs b/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs index 27f908e1..6b7fbef3 100644 --- a/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs +++ b/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs @@ -21,4 +21,20 @@ internal interface IBatchGeospatialCommands /// /// Command Response - IBatch GeoDistance(ValkeyKey key, ValkeyValue member1, ValkeyValue member2, GeoUnit unit = GeoUnit.Meters); + + /// + /// Command Response - + IBatch GeoHash(ValkeyKey key, ValkeyValue member); + + /// + /// Command Response - + IBatch GeoHash(ValkeyKey key, ValkeyValue[] members); + + /// + /// Command Response - + IBatch GeoPosition(ValkeyKey key, ValkeyValue member); + + /// + /// Command Response - + IBatch GeoPosition(ValkeyKey key, ValkeyValue[] members); } \ No newline at end of file diff --git a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs index f1904cbf..31e3f81b 100644 --- a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs @@ -89,4 +89,95 @@ public async Task GeoDistance_NonExistentMember_ReturnsNull(BaseClient client) double? distance = await client.GeoDistanceAsync(key, "Palermo", "NonExistent"); Assert.Null(distance); } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoHash_SingleMember_ReturnsHash(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo"); + + string? hash = await client.GeoHashAsync(key, "Palermo"); + Assert.NotNull(hash); + Assert.NotEmpty(hash); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoHash_MultipleMembers_ReturnsHashes(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + await client.GeoAddAsync(key, entries); + + string?[] hashes = await client.GeoHashAsync(key, new ValkeyValue[] { "Palermo", "Catania" }); + Assert.Equal(2, hashes.Length); + Assert.NotNull(hashes[0]); + Assert.NotNull(hashes[1]); + Assert.NotEmpty(hashes[0]); + Assert.NotEmpty(hashes[1]); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoHash_NonExistentMember_ReturnsNull(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo"); + + string? hash = await client.GeoHashAsync(key, "NonExistent"); + Assert.Null(hash); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoPosition_SingleMember_ReturnsPosition(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + double longitude = 13.361389; + double latitude = 38.115556; + await client.GeoAddAsync(key, longitude, latitude, "Palermo"); + + GeoPosition? position = await client.GeoPositionAsync(key, "Palermo"); + Assert.NotNull(position); + Assert.True(Math.Abs(position.Value.Longitude - longitude) < 0.001); + Assert.True(Math.Abs(position.Value.Latitude - latitude) < 0.001); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoPosition_MultipleMembers_ReturnsPositions(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + await client.GeoAddAsync(key, entries); + + GeoPosition?[] positions = await client.GeoPositionAsync(key, new ValkeyValue[] { "Palermo", "Catania" }); + Assert.Equal(2, positions.Length); + Assert.NotNull(positions[0]); + Assert.NotNull(positions[1]); + Assert.True(Math.Abs(positions[0]!.Value.Longitude - 13.361389) < 0.001); + Assert.True(Math.Abs(positions[0]!.Value.Latitude - 38.115556) < 0.001); + Assert.True(Math.Abs(positions[1]!.Value.Longitude - 15.087269) < 0.001); + Assert.True(Math.Abs(positions[1]!.Value.Latitude - 37.502669) < 0.001); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoPosition_NonExistentMember_ReturnsNull(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo"); + + GeoPosition? position = await client.GeoPositionAsync(key, "NonExistent"); + Assert.Null(position); + } } \ No newline at end of file From 4b287273aa7f95e72158cb31ec0a9f39e3c2eeda Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Fri, 10 Oct 2025 10:29:41 -0700 Subject: [PATCH 03/27] GeoSearch Signed-off-by: Alex Rehnby-Martin --- .../BaseClient.GeospatialCommands.cs | 21 ++ .../Commands/IGeospatialCommands.cs | 69 ++++++ .../Internals/Request.GeospatialCommands.cs | 219 ++++++++++++++++++ .../Pipeline/BaseBatch.GeospatialCommands.cs | 12 + .../Pipeline/IBatchGeospatialCommands.cs | 12 + .../abstract_APITypes/GeoSearchShape.cs | 30 +++ .../abstract_APITypes/ValkeyLiterals.cs | 1 + 7 files changed, 364 insertions(+) diff --git a/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs b/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs index d0f5d8d7..835c48c0 100644 --- a/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs +++ b/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs @@ -61,4 +61,25 @@ public async Task GeoAddAsync(ValkeyKey key, GeoEntry[] values, CommandFla Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); return await Command(Request.GeoPositionAsync(key, members)); } + + /// + public async Task GeoSearchAsync(ValkeyKey key, ValkeyValue fromMember, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.GeoSearchAsync(key, fromMember, shape, count, demandClosest, order, options)); + } + + /// + public async Task GeoSearchAsync(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.GeoSearchAsync(key, fromPosition, shape, count, demandClosest, order, options)); + } + + /// + public async Task GeoSearchAsync(ValkeyKey key, GeoSearchPolygon polygon, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.GeoSearchAsync(key, polygon, count, demandClosest, order, options)); + } } \ No newline at end of file diff --git a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs index 1eb87bca..4cdbde79 100644 --- a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs +++ b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs @@ -157,4 +157,73 @@ public interface IGeospatialCommands /// /// Task GeoPositionAsync(ValkeyKey key, ValkeyValue[] members, CommandFlags flags = CommandFlags.None); + + /// + /// Returns the members of a geospatial index which are within the borders of the area specified by a given shape. + /// + /// + /// The key of the sorted set. + /// The member to search from. + /// The search area shape. + /// The maximum number of results to return. Use -1 for no limit. + /// When true, returns the closest results. When false, allows any results. + /// The order in which to return results. Null for default ordering. + /// The options for the search result format. + /// The flags to use for this operation. Currently flags are ignored. + /// An array of results within the specified area. + /// + /// + /// + /// var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); + /// GeoRadiusResult[] results = await client.GeoSearchAsync("mygeo", "Palermo", shape); + /// + /// + /// + Task GeoSearchAsync(ValkeyKey key, ValkeyValue fromMember, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); + + /// + /// Returns the members of a geospatial index which are within the borders of the area specified by a given shape. + /// + /// + /// The key of the sorted set. + /// The position to search from. + /// The search area shape. + /// The maximum number of results to return. Use -1 for no limit. + /// When true, returns the closest results. When false, allows any results. + /// The order in which to return results. Null for default ordering. + /// The options for the search result format. + /// The flags to use for this operation. Currently flags are ignored. + /// An array of results within the specified area. + /// + /// + /// + /// var position = new GeoPosition(15.087269, 37.502669); + /// var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); + /// GeoRadiusResult[] results = await client.GeoSearchAsync("mygeo", position, shape); + /// + /// + /// + Task GeoSearchAsync(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); + + /// + /// Returns the members of a geospatial index which are within the borders of the specified polygon. + /// + /// + /// The key of the sorted set. + /// The polygon area to search within. + /// The maximum number of results to return. Use -1 for no limit. + /// When true, returns the closest results. When false, allows any results. + /// The order in which to return results. Null for default ordering. + /// The options for the search result format. + /// The flags to use for this operation. Currently flags are ignored. + /// An array of results within the specified polygon. + /// + /// + /// + /// var polygon = new GeoSearchPolygon(new GeoPosition[] { new(12.0, 37.0), new(16.0, 37.0), new(16.0, 39.0), new(12.0, 39.0) }); + /// GeoRadiusResult[] results = await client.GeoSearchAsync("mygeo", polygon); + /// + /// + /// + Task GeoSearchAsync(ValkeyKey key, GeoSearchPolygon polygon, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); } \ No newline at end of file diff --git a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs index 54573183..e1dfc4c5 100644 --- a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs @@ -114,4 +114,223 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values) }).ToArray(); }); } + + /// + /// Creates a request for GEOSEARCH command with member origin. + /// + /// The key of the sorted set. + /// The member to search from. + /// The search area shape. + /// A with the request. + public static Cmd GeoSearchAsync(ValkeyKey key, ValkeyValue fromMember, GeoSearchShape shape) + { + return GeoSearchAsync(key, fromMember, shape, -1, true, null, GeoRadiusOptions.None); + } + + /// + /// Creates a request for GEOSEARCH command with member origin, count limit, demandClosest option, order, and options. + /// + /// The key of the sorted set. + /// The member to search from. + /// The search area shape. + /// The maximum number of results to return. + /// When true, returns the closest results. When false, allows any results. + /// The order in which to return results. + /// The options for the search result format. + /// A with the request. + public static Cmd GeoSearchAsync(ValkeyKey key, ValkeyValue fromMember, GeoSearchShape shape, long count, bool demandClosest, Order? order, GeoRadiusOptions options) + { + List args = [key.ToGlideString(), ValkeyLiterals.FROMMEMBER.ToGlideString(), fromMember.ToGlideString()]; + List shapeArgs = []; + shape.AddArgs(shapeArgs); + args.AddRange(shapeArgs.Select(a => a.ToGlideString())); + if (count > 0) + { + args.Add(ValkeyLiterals.COUNT.ToGlideString()); + args.Add(count.ToGlideString()); + if (!demandClosest) + { + args.Add(ValkeyLiterals.ANY.ToGlideString()); + } + } + if (order.HasValue) + { + args.Add(order.Value.ToLiteral().ToGlideString()); + } + List optionArgs = []; + options.AddArgs(optionArgs); + args.AddRange(optionArgs.Select(a => a.ToGlideString())); + return new(RequestType.GeoSearch, [.. args], false, response => ProcessGeoSearchResponse(response, options)); + } + + /// + /// Creates a request for GEOSEARCH command with position origin. + /// + /// The key of the sorted set. + /// The position to search from. + /// The search area shape. + /// A with the request. + public static Cmd GeoSearchAsync(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape) + { + return GeoSearchAsync(key, fromPosition, shape, -1, true, null, GeoRadiusOptions.None); + } + + /// + /// Creates a request for GEOSEARCH command with position origin, count limit, demandClosest option, order, and options. + /// + /// The key of the sorted set. + /// The position to search from. + /// The search area shape. + /// The maximum number of results to return. + /// When true, returns the closest results. When false, allows any results. + /// The order in which to return results. + /// The options for the search result format. + /// A with the request. + public static Cmd GeoSearchAsync(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape, long count, bool demandClosest, Order? order, GeoRadiusOptions options) + { + List args = [key.ToGlideString(), ValkeyLiterals.FROMLONLAT.ToGlideString(), fromPosition.Longitude.ToGlideString(), fromPosition.Latitude.ToGlideString()]; + List shapeArgs = []; + shape.AddArgs(shapeArgs); + args.AddRange(shapeArgs.Select(a => a.ToGlideString())); + if (count > 0) + { + args.Add(ValkeyLiterals.COUNT.ToGlideString()); + args.Add(count.ToGlideString()); + if (!demandClosest) + { + args.Add(ValkeyLiterals.ANY.ToGlideString()); + } + } + if (order.HasValue) + { + args.Add(order.Value.ToLiteral().ToGlideString()); + } + List optionArgs = []; + options.AddArgs(optionArgs); + args.AddRange(optionArgs.Select(a => a.ToGlideString())); + return new(RequestType.GeoSearch, [.. args], false, response => ProcessGeoSearchResponse(response, options)); + } + + /// + /// Creates a request for GEOSEARCH command with polygon. + /// + /// The key of the sorted set. + /// The polygon to search within. + /// A with the request. + public static Cmd GeoSearchAsync(ValkeyKey key, GeoSearchPolygon polygon) + { + return GeoSearchAsync(key, polygon, -1, true, null, GeoRadiusOptions.None); + } + + /// + /// Creates a request for GEOSEARCH command with polygon, count limit, demandClosest option, order, and options. + /// + /// The key of the sorted set. + /// The polygon to search within. + /// The maximum number of results to return. + /// When true, returns the closest results. When false, allows any results. + /// The order in which to return results. + /// The options for the search result format. + /// A with the request. + public static Cmd GeoSearchAsync(ValkeyKey key, GeoSearchPolygon polygon, long count, bool demandClosest, Order? order, GeoRadiusOptions options) + { + List args = [key.ToGlideString()]; + List polygonArgs = []; + polygon.AddArgs(polygonArgs); + args.AddRange(polygonArgs.Select(a => a.ToGlideString())); + if (count > 0) + { + args.Add(ValkeyLiterals.COUNT.ToGlideString()); + args.Add(count.ToGlideString()); + if (!demandClosest) + { + args.Add(ValkeyLiterals.ANY.ToGlideString()); + } + } + if (order.HasValue) + { + args.Add(order.Value.ToLiteral().ToGlideString()); + } + List optionArgs = []; + options.AddArgs(optionArgs); + args.AddRange(optionArgs.Select(a => a.ToGlideString())); + return new(RequestType.GeoSearch, [.. args], false, response => ProcessGeoSearchResponse(response, options)); + } + + /// + /// Processes the response from GEOSEARCH command based on the options. + /// + /// The raw response from the server. + /// The options used in the request. + /// An array of GeoRadiusResult objects. + private static GeoRadiusResult[] ProcessGeoSearchResponse(object[] response, GeoRadiusOptions options) + { + + + return response.Select(item => + { + // If no options are specified, Redis returns simple strings (member names) + if (options == GeoRadiusOptions.None) + { + return new GeoRadiusResult(new ValkeyValue(item?.ToString()), null, null, null); + } + + // With options, Redis returns arrays: [member, ...additional data based on options] + if (item is not object[] itemArray || itemArray.Length == 0) + { + // Fallback for unexpected format + return new GeoRadiusResult(new ValkeyValue(item?.ToString()), null, null, null); + } + + var member = new ValkeyValue(itemArray[0]?.ToString()); + double? distance = null; + long? hash = null; + GeoPosition? position = null; + + int index = 1; + + // Redis returns additional data in this specific order: + // 1. Distance (if WITHDIST) + // 2. Hash (if WITHHASH) + // 3. Coordinates (if WITHCOORD) + + if ((options & GeoRadiusOptions.WithDistance) != 0 && index < itemArray.Length) + { + // Distance comes as a nested array: [distance_value] + if (itemArray[index] is object[] distArray && distArray.Length > 0) + { + if (double.TryParse(distArray[0]?.ToString(), out var dist)) + distance = dist; + } + index++; + } + + if ((options & GeoRadiusOptions.WithGeoHash) != 0 && index < itemArray.Length) + { + // Hash comes as a nested array: [hash_value] + if (itemArray[index] is object[] hashArray && hashArray.Length > 0) + { + if (long.TryParse(hashArray[0]?.ToString(), out var h)) + hash = h; + } + index++; + } + + if ((options & GeoRadiusOptions.WithCoordinates) != 0 && index < itemArray.Length) + { + // Coordinates come as a triple-nested array: [[[longitude, latitude]]] + if (itemArray[index] is object[] coordOuterArray && coordOuterArray.Length > 0 && + coordOuterArray[0] is object[] coordMiddleArray && coordMiddleArray.Length >= 2) + { + if (double.TryParse(coordMiddleArray[0]?.ToString(), out var lon) && + double.TryParse(coordMiddleArray[1]?.ToString(), out var lat)) + { + position = new GeoPosition(lon, lat); + } + } + } + + return new GeoRadiusResult(member, distance, hash, position); + }).ToArray(); + } } \ No newline at end of file diff --git a/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs b/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs index 3a304035..1bf827a2 100644 --- a/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs +++ b/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs @@ -33,6 +33,15 @@ public abstract partial class BaseBatch /// public T GeoPosition(ValkeyKey key, ValkeyValue[] members) => AddCmd(GeoPositionAsync(key, members)); + /// + public T GeoSearch(ValkeyKey key, ValkeyValue fromMember, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default) => AddCmd(GeoSearchAsync(key, fromMember, shape, count, demandClosest, order, options)); + + /// + public T GeoSearch(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default) => AddCmd(GeoSearchAsync(key, fromPosition, shape, count, demandClosest, order, options)); + + /// + public T GeoSearch(ValkeyKey key, GeoSearchPolygon polygon, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default) => AddCmd(GeoSearchAsync(key, polygon, count, demandClosest, order, options)); + // Explicit interface implementations for IBatchGeospatialCommands IBatch IBatchGeospatialCommands.GeoAdd(ValkeyKey key, double longitude, double latitude, ValkeyValue member) => GeoAdd(key, longitude, latitude, member); IBatch IBatchGeospatialCommands.GeoAdd(ValkeyKey key, GeoEntry value) => GeoAdd(key, value); @@ -42,4 +51,7 @@ public abstract partial class BaseBatch IBatch IBatchGeospatialCommands.GeoHash(ValkeyKey key, ValkeyValue[] members) => GeoHash(key, members); IBatch IBatchGeospatialCommands.GeoPosition(ValkeyKey key, ValkeyValue member) => GeoPosition(key, member); IBatch IBatchGeospatialCommands.GeoPosition(ValkeyKey key, ValkeyValue[] members) => GeoPosition(key, members); + IBatch IBatchGeospatialCommands.GeoSearch(ValkeyKey key, ValkeyValue fromMember, GeoSearchShape shape, long count, bool demandClosest, Order? order, GeoRadiusOptions options) => GeoSearch(key, fromMember, shape, count, demandClosest, order, options); + IBatch IBatchGeospatialCommands.GeoSearch(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape, long count, bool demandClosest, Order? order, GeoRadiusOptions options) => GeoSearch(key, fromPosition, shape, count, demandClosest, order, options); + IBatch IBatchGeospatialCommands.GeoSearch(ValkeyKey key, GeoSearchPolygon polygon, long count, bool demandClosest, Order? order, GeoRadiusOptions options) => GeoSearch(key, polygon, count, demandClosest, order, options); } \ No newline at end of file diff --git a/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs b/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs index 6b7fbef3..b3f50aed 100644 --- a/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs +++ b/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs @@ -37,4 +37,16 @@ internal interface IBatchGeospatialCommands /// /// Command Response - IBatch GeoPosition(ValkeyKey key, ValkeyValue[] members); + + /// + /// Command Response - + IBatch GeoSearch(ValkeyKey key, ValkeyValue fromMember, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default); + + /// + /// Command Response - + IBatch GeoSearch(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default); + + /// + /// Command Response - + IBatch GeoSearch(ValkeyKey key, GeoSearchPolygon polygon, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default); } \ No newline at end of file diff --git a/sources/Valkey.Glide/abstract_APITypes/GeoSearchShape.cs b/sources/Valkey.Glide/abstract_APITypes/GeoSearchShape.cs index 20cb0c7d..ebafa8f0 100644 --- a/sources/Valkey.Glide/abstract_APITypes/GeoSearchShape.cs +++ b/sources/Valkey.Glide/abstract_APITypes/GeoSearchShape.cs @@ -90,3 +90,33 @@ internal override void AddArgs(List args) args.Add(Unit.ToLiteral()); } } + +/// +/// A polygon drawn on a map. +/// +public class GeoSearchPolygon : GeoSearchShape +{ + private readonly GeoPosition[] _vertices; + + /// + /// Initializes a GeoPolygon. + /// + /// The vertices of the polygon. + public GeoSearchPolygon(GeoPosition[] vertices) : base(GeoUnit.Meters) + { + _vertices = vertices; + } + + internal override int ArgCount => 2 + (_vertices.Length * 2); + + internal override void AddArgs(List args) + { + args.Add(ValkeyLiterals.BYPOLYGON); + args.Add(_vertices.Length); + foreach (var vertex in _vertices) + { + args.Add(vertex.Longitude); + args.Add(vertex.Latitude); + } + } +} diff --git a/sources/Valkey.Glide/abstract_APITypes/ValkeyLiterals.cs b/sources/Valkey.Glide/abstract_APITypes/ValkeyLiterals.cs index 1bb0fea1..631dfd0b 100644 --- a/sources/Valkey.Glide/abstract_APITypes/ValkeyLiterals.cs +++ b/sources/Valkey.Glide/abstract_APITypes/ValkeyLiterals.cs @@ -136,6 +136,7 @@ public static readonly ValkeyValue // Geo Radius/Search Literals BYBOX = "BYBOX", + BYPOLYGON = "BYPOLYGON", BYRADIUS = "BYRADIUS", FROMMEMBER = "FROMMEMBER", FROMLONLAT = "FROMLONLAT", From 4eae3cacfaf7971bb490e95a0461fc5422dd4447 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Fri, 10 Oct 2025 10:30:01 -0700 Subject: [PATCH 04/27] GeoSearch Signed-off-by: Alex Rehnby-Martin --- .../GeospatialCommandTests.cs | 261 ++++++++++++++++++ 1 file changed, 261 insertions(+) diff --git a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs index 31e3f81b..27a7be41 100644 --- a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs @@ -180,4 +180,265 @@ public async Task GeoPosition_NonExistentMember_ReturnsNull(BaseClient client) GeoPosition? position = await client.GeoPositionAsync(key, "NonExistent"); Assert.Null(position); } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearch_FromMember_ByRadius_ReturnsMembers(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania"), + new GeoEntry(12.758489, 38.788135, "edge") + ]; + await client.GeoAddAsync(key, entries); + + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); + GeoRadiusResult[] results = await client.GeoSearchAsync(key, "Palermo", shape); + + Assert.NotEmpty(results); + Assert.Contains("Palermo", results.Select(r => r.Member.ToString())); + Assert.Contains("Catania", results.Select(r => r.Member.ToString())); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearch_FromPosition_ByRadius_ReturnsMembers(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + await client.GeoAddAsync(key, entries); + + var position = new GeoPosition(13.361389, 38.115556); // Palermo coordinates + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); + GeoRadiusResult[] results = await client.GeoSearchAsync(key, position, shape); + + Assert.NotEmpty(results); + Assert.Contains("Palermo", results.Select(r => r.Member.ToString())); + Assert.Contains("Catania", results.Select(r => r.Member.ToString())); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearch_FromMember_ByBox_ReturnsMembers(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania"), + new GeoEntry(12.758489, 38.788135, "edge") + ]; + await client.GeoAddAsync(key, entries); + + var shape = new GeoSearchBox(400, 400, GeoUnit.Kilometers); + GeoRadiusResult[] results = await client.GeoSearchAsync(key, "Palermo", shape); + + Assert.NotEmpty(results); + Assert.Contains("Palermo", results.Select(r => r.Member.ToString())); + Assert.Contains("Catania", results.Select(r => r.Member.ToString())); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearch_FromPosition_ByBox_ReturnsMembers(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + await client.GeoAddAsync(key, entries); + + var position = new GeoPosition(13.361389, 38.115556); // Palermo coordinates + var shape = new GeoSearchBox(400, 400, GeoUnit.Kilometers); + GeoRadiusResult[] results = await client.GeoSearchAsync(key, position, shape); + + Assert.NotEmpty(results); + Assert.Contains("Palermo", results.Select(r => r.Member.ToString())); + Assert.Contains("Catania", results.Select(r => r.Member.ToString())); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearch_ByPolygon_ReturnsMembers(BaseClient client) + { + Assert.SkipWhen( + TestConfiguration.SERVER_VERSION < new Version(9, 0, 0), + "BYPOLYGON is only supported in Valkey 9.0.0+" + ); + + string key = Guid.NewGuid().ToString(); + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + await client.GeoAddAsync(key, entries); + + var vertices = new GeoPosition[] + { + new GeoPosition(12.0, 37.0), + new GeoPosition(16.0, 37.0), + new GeoPosition(16.0, 39.0), + new GeoPosition(12.0, 39.0) + }; + var polygon = new GeoSearchPolygon(vertices); + GeoRadiusResult[] results = await client.GeoSearchAsync(key, polygon); + + Assert.NotEmpty(results); + Assert.Contains("Palermo", results.Select(r => r.Member.ToString())); + Assert.Contains("Catania", results.Select(r => r.Member.ToString())); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearch_WithCount_LimitsResults(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania"), + new GeoEntry(12.758489, 38.788135, "edge1"), + new GeoEntry(14.015482, 37.734741, "edge2") + ]; + await client.GeoAddAsync(key, entries); + + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); + GeoRadiusResult[] allResults = await client.GeoSearchAsync(key, "Palermo", shape); + GeoRadiusResult[] limitedResults = await client.GeoSearchAsync(key, "Palermo", shape, 2); + + Assert.True(allResults.Length >= 2); + Assert.Equal(2, limitedResults.Length); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearch_WithDemandClosest_VerifiesParameterUsage(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania"), + new GeoEntry(12.758489, 38.788135, "edge1"), + new GeoEntry(14.015482, 37.734741, "edge2"), + new GeoEntry(13.5, 38.0, "close1") + ]; + await client.GeoAddAsync(key, entries); + + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); + + // Test that demandClosest=true works (should return closest results) + GeoRadiusResult[] closestResults = await client.GeoSearchAsync(key, "Palermo", shape, 3, true); + Assert.Equal(3, closestResults.Length); + Assert.Contains("Palermo", closestResults.Select(r => r.Member.ToString())); + Assert.Contains("close1", closestResults.Select(r => r.Member.ToString())); // close1 should be in closest results + + // Test that demandClosest=false works (should return any results, not necessarily closest) + GeoRadiusResult[] anyResults = await client.GeoSearchAsync(key, "Palermo", shape, 3, false); + Assert.Equal(3, anyResults.Length); + Assert.Contains("Palermo", anyResults.Select(r => r.Member.ToString())); + + // Both should return valid results, verifying the parameter is accepted + Assert.NotEmpty(closestResults); + Assert.NotEmpty(anyResults); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearch_WithOrder_ReturnsOrderedResults(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania"), + new GeoEntry(12.758489, 38.788135, "edge1") + ]; + await client.GeoAddAsync(key, entries); + + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); + + // Test ascending order + GeoRadiusResult[] ascResults = await client.GeoSearchAsync(key, "Palermo", shape, order: Order.Ascending); + Assert.NotEmpty(ascResults); + + // Test descending order + GeoRadiusResult[] descResults = await client.GeoSearchAsync(key, "Palermo", shape, order: Order.Descending); + Assert.NotEmpty(descResults); + + // Verify both return same count but potentially different order + Assert.Equal(ascResults.Length, descResults.Length); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearch_WithOptions_ReturnsEnrichedResults(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + await client.GeoAddAsync(key, entries); + + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); + + // Test with distance option + GeoRadiusResult[] distResults = await client.GeoSearchAsync(key, "Palermo", shape, options: GeoRadiusOptions.WithDistance); + Assert.NotEmpty(distResults); + + var palermoDist = distResults.FirstOrDefault(r => r.Member.ToString() == "Palermo"); + Assert.NotNull(palermoDist); + Assert.Equal("Palermo", palermoDist.Member.ToString()); + Assert.NotNull(palermoDist.Distance); // Should have distance + Assert.Equal(0.0, palermoDist.Distance.Value, 1); // Distance from itself should be ~0 + Assert.Null(palermoDist.Position); // Should be null without WithCoordinates + + // Test with coordinates option + GeoRadiusResult[] coordResults = await client.GeoSearchAsync(key, "Palermo", shape, options: GeoRadiusOptions.WithCoordinates); + Assert.NotEmpty(coordResults); + + var palermoCoord = coordResults.FirstOrDefault(r => r.Member.ToString() == "Palermo"); + Assert.NotNull(palermoCoord); + Assert.Equal("Palermo", palermoCoord.Member.ToString()); + Assert.True(palermoCoord.Position.HasValue); // Should have coordinates + Assert.Null(palermoCoord.Distance); // Should be null without WithDistance + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearch_WithDistance_ReturnsAccurateDistances(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + await client.GeoAddAsync(key, entries); + + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); + GeoRadiusResult[] results = await client.GeoSearchAsync(key, "Palermo", shape, options: GeoRadiusOptions.WithDistance); + + var palermoResult = results.FirstOrDefault(r => r.Member.ToString() == "Palermo"); + var cataniaResult = results.FirstOrDefault(r => r.Member.ToString() == "Catania"); + + Assert.NotNull(palermoResult); + Assert.NotNull(cataniaResult); + Assert.NotNull(palermoResult.Distance); + Assert.NotNull(cataniaResult.Distance); + + Assert.Equal(0.0, palermoResult.Distance.Value, 1); // Distance from itself should be ~0 + Assert.True(cataniaResult.Distance.Value > 160 && cataniaResult.Distance.Value < 170); // ~166km between cities + } } \ No newline at end of file From 6c5c594fef6cf8e9191f101fa25cb78a865713c2 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Mon, 13 Oct 2025 10:05:59 -0700 Subject: [PATCH 05/27] Add comprehensive invalid argument testing Signed-off-by: Alex Rehnby-Martin --- .../BaseClient.GeospatialCommands.cs | 14 ++ .../Commands/IGeospatialCommands.cs | 4 + .../Internals/Request.GeospatialCommands.cs | 76 ++++++ .../GeospatialCommandTests.cs | 237 ++++++++++++++++++ 4 files changed, 331 insertions(+) diff --git a/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs b/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs index 835c48c0..97403ed1 100644 --- a/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs +++ b/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs @@ -82,4 +82,18 @@ public async Task GeoSearchAsync(ValkeyKey key, GeoSearchPoly Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); return await Command(Request.GeoSearchAsync(key, polygon, count, demandClosest, order, options)); } + + /// + public async Task GeoSearchAndStoreAsync(ValkeyKey sourceKey, ValkeyKey destinationKey, ValkeyValue fromMember, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.GeoSearchAndStoreAsync(sourceKey, destinationKey, fromMember, shape, count, demandClosest, order, storeDistances)); + } + + /// + public async Task GeoSearchAndStoreAsync(ValkeyKey sourceKey, ValkeyKey destinationKey, GeoPosition fromPosition, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.GeoSearchAndStoreAsync(sourceKey, destinationKey, fromPosition, shape, count, demandClosest, order, storeDistances)); + } } \ No newline at end of file diff --git a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs index 4cdbde79..ff1d9482 100644 --- a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs +++ b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs @@ -226,4 +226,8 @@ public interface IGeospatialCommands /// /// Task GeoSearchAsync(ValkeyKey key, GeoSearchPolygon polygon, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); + + Task GeoSearchAndStoreAsync(ValkeyKey sourceKey, ValkeyKey destinationKey, ValkeyValue fromMember, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None); + + Task GeoSearchAndStoreAsync(ValkeyKey sourceKey, ValkeyKey destinationKey, GeoPosition fromPosition, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None); } \ No newline at end of file diff --git a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs index e1dfc4c5..0ef208e4 100644 --- a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs @@ -333,4 +333,80 @@ private static GeoRadiusResult[] ProcessGeoSearchResponse(object[] response, Geo return new GeoRadiusResult(member, distance, hash, position); }).ToArray(); } + + /// + /// Creates a request for GEOSEARCHSTORE command with member origin. + /// + /// The key of the source sorted set. + /// The key where results will be stored. + /// The member to search from. + /// The search area shape. + /// The maximum number of results to return. + /// When true, returns the closest results. When false, allows any results. + /// The order in which to return results. + /// When true, stores distances instead of just member names. + /// A with the request. + public static Cmd GeoSearchAndStoreAsync(ValkeyKey sourceKey, ValkeyKey destinationKey, ValkeyValue fromMember, GeoSearchShape shape, long count, bool demandClosest, Order? order, bool storeDistances) + { + List args = [destinationKey.ToGlideString(), sourceKey.ToGlideString(), ValkeyLiterals.FROMMEMBER.ToGlideString(), fromMember.ToGlideString()]; + List shapeArgs = []; + shape.AddArgs(shapeArgs); + args.AddRange(shapeArgs.Select(a => a.ToGlideString())); + if (count > 0) + { + args.Add(ValkeyLiterals.COUNT.ToGlideString()); + args.Add(count.ToGlideString()); + if (!demandClosest) + { + args.Add(ValkeyLiterals.ANY.ToGlideString()); + } + } + if (order.HasValue) + { + args.Add(order.Value.ToLiteral().ToGlideString()); + } + if (storeDistances) + { + args.Add(ValkeyLiterals.STOREDIST.ToGlideString()); + } + return Simple(RequestType.GeoSearchStore, [.. args]); + } + + /// + /// Creates a request for GEOSEARCHSTORE command with position origin. + /// + /// The key of the source sorted set. + /// The key where results will be stored. + /// The position to search from. + /// The search area shape. + /// The maximum number of results to return. + /// When true, returns the closest results. When false, allows any results. + /// The order in which to return results. + /// When true, stores distances instead of just member names. + /// A with the request. + public static Cmd GeoSearchAndStoreAsync(ValkeyKey sourceKey, ValkeyKey destinationKey, GeoPosition fromPosition, GeoSearchShape shape, long count, bool demandClosest, Order? order, bool storeDistances) + { + List args = [destinationKey.ToGlideString(), sourceKey.ToGlideString(), ValkeyLiterals.FROMLONLAT.ToGlideString(), fromPosition.Longitude.ToGlideString(), fromPosition.Latitude.ToGlideString()]; + List shapeArgs = []; + shape.AddArgs(shapeArgs); + args.AddRange(shapeArgs.Select(a => a.ToGlideString())); + if (count > 0) + { + args.Add(ValkeyLiterals.COUNT.ToGlideString()); + args.Add(count.ToGlideString()); + if (!demandClosest) + { + args.Add(ValkeyLiterals.ANY.ToGlideString()); + } + } + if (order.HasValue) + { + args.Add(order.Value.ToLiteral().ToGlideString()); + } + if (storeDistances) + { + args.Add(ValkeyLiterals.STOREDIST.ToGlideString()); + } + return Simple(RequestType.GeoSearchStore, [.. args]); + } } \ No newline at end of file diff --git a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs index 27a7be41..dffb60bc 100644 --- a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs @@ -61,6 +61,60 @@ public async Task GeoAdd_DuplicateEntry_ReturnsFalse(BaseClient client) Assert.False(secondResult); } + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoAdd_InvalidLongitude_ThrowsException(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + string member = "InvalidPlace"; + + // Test longitude too low (-181) + await Assert.ThrowsAsync(async () => + await client.GeoAddAsync(key, -181, 0, member)); + + // Test longitude too high (181) + await Assert.ThrowsAsync(async () => + await client.GeoAddAsync(key, 181, 0, member)); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoAdd_InvalidLatitude_ThrowsException(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + string member = "InvalidPlace"; + + // Test latitude too high (86) + await Assert.ThrowsAsync(async () => + await client.GeoAddAsync(key, 0, 86, member)); + + // Test latitude too low (-86) + await Assert.ThrowsAsync(async () => + await client.GeoAddAsync(key, 0, -86, member)); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoAdd_EmptyEntries_ThrowsException(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + GeoEntry[] emptyEntries = []; + + await Assert.ThrowsAsync(async () => + await client.GeoAddAsync(key, emptyEntries)); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoAdd_WrongKeyType_ThrowsException(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + await client.StringSetAsync(key, "not_a_geo_key"); + + await Assert.ThrowsAsync(async () => + await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo")); + } + [Theory(DisableDiscoveryEnumeration = true)] [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task GeoDistance_ReturnsCorrectDistance(BaseClient client) @@ -90,6 +144,17 @@ public async Task GeoDistance_NonExistentMember_ReturnsNull(BaseClient client) Assert.Null(distance); } + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoDistance_WrongKeyType_ThrowsException(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + await client.StringSetAsync(key, "not_a_geo_key"); + + await Assert.ThrowsAsync(async () => + await client.GeoDistanceAsync(key, "member1", "member2")); + } + [Theory(DisableDiscoveryEnumeration = true)] [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task GeoHash_SingleMember_ReturnsHash(BaseClient client) @@ -133,6 +198,28 @@ public async Task GeoHash_NonExistentMember_ReturnsNull(BaseClient client) Assert.Null(hash); } + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoHash_WrongKeyType_ThrowsException(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + await client.StringSetAsync(key, "not_a_geo_key"); + + await Assert.ThrowsAsync(async () => + await client.GeoHashAsync(key, "member")); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoHash_EmptyMembers_ReturnsEmptyArray(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo"); + + string?[] hashes = await client.GeoHashAsync(key, new ValkeyValue[] { }); + Assert.Empty(hashes); + } + [Theory(DisableDiscoveryEnumeration = true)] [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task GeoPosition_SingleMember_ReturnsPosition(BaseClient client) @@ -181,6 +268,17 @@ public async Task GeoPosition_NonExistentMember_ReturnsNull(BaseClient client) Assert.Null(position); } + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoPosition_WrongKeyType_ThrowsException(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + await client.StringSetAsync(key, "not_a_geo_key"); + + await Assert.ThrowsAsync(async () => + await client.GeoPositionAsync(key, "member")); + } + [Theory(DisableDiscoveryEnumeration = true)] [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task GeoSearch_FromMember_ByRadius_ReturnsMembers(BaseClient client) @@ -441,4 +539,143 @@ public async Task GeoSearch_WithDistance_ReturnsAccurateDistances(BaseClient cli Assert.Equal(0.0, palermoResult.Distance.Value, 1); // Distance from itself should be ~0 Assert.True(cataniaResult.Distance.Value > 160 && cataniaResult.Distance.Value < 170); // ~166km between cities } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearchAndStore_WithMember_StoresResults(BaseClient client) + { + string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; + string sourceKey = keyPrefix + ":source"; + string destinationKey = keyPrefix + ":dest"; + + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + await client.GeoAddAsync(sourceKey, entries); + + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); + long count = await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, "Palermo", shape); + + Assert.True(count >= 1); + Assert.Contains("Palermo", (await client.SortedSetRangeByRankAsync(destinationKey, 0, -1)).Select(r => r.ToString())); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearchAndStore_WithPosition_StoresResults(BaseClient client) + { + string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; + string sourceKey = keyPrefix + ":source"; + string destinationKey = keyPrefix + ":dest"; + + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + await client.GeoAddAsync(sourceKey, entries); + + var position = new GeoPosition(13.361389, 38.115556); + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); + long count = await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, position, shape); + + Assert.True(count >= 1); + Assert.Contains("Palermo", (await client.SortedSetRangeByRankAsync(destinationKey, 0, -1)).Select(r => r.ToString())); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearchAndStore_WithDistances_StoresDistances(BaseClient client) + { + string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; + string sourceKey = keyPrefix + ":source"; + string destinationKey = keyPrefix + ":dest"; + + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + await client.GeoAddAsync(sourceKey, entries); + + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); + long count = await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, "Palermo", shape, storeDistances: true); + + Assert.True(count >= 1); + var results = await client.SortedSetRangeByRankWithScoresAsync(destinationKey, 0, -1); + Assert.NotEmpty(results); + Assert.True(results.Any(r => r.Score >= 0)); // Distances should be non-negative + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearch_NonExistentMember_ThrowsException(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo"); + + var shape = new GeoSearchCircle(100, GeoUnit.Kilometers); + await Assert.ThrowsAsync(async () => + await client.GeoSearchAsync(key, "NonExistentMember", shape)); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearch_WrongKeyType_ThrowsException(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + await client.StringSetAsync(key, "not_a_geo_key"); + + var position = new GeoPosition(13.361389, 38.115556); + var shape = new GeoSearchCircle(100, GeoUnit.Kilometers); + await Assert.ThrowsAsync(async () => + await client.GeoSearchAsync(key, position, shape)); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearch_NoMembersInArea_ReturnsEmpty(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo"); + + var position = new GeoPosition(0.0, 0.0); // Far from Palermo + var shape = new GeoSearchCircle(1, GeoUnit.Meters); // Very small radius + GeoRadiusResult[] results = await client.GeoSearchAsync(key, position, shape); + + Assert.Empty(results); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearchAndStore_NonExistentMember_ThrowsException(BaseClient client) + { + string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; + string sourceKey = keyPrefix + ":source"; + string destinationKey = keyPrefix + ":dest"; + + await client.GeoAddAsync(sourceKey, 13.361389, 38.115556, "Palermo"); + + var shape = new GeoSearchCircle(100, GeoUnit.Kilometers); + await Assert.ThrowsAsync(async () => + await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, "NonExistentMember", shape)); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearchAndStore_WrongKeyType_ThrowsException(BaseClient client) + { + string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; + string sourceKey = keyPrefix + ":source"; + string destinationKey = keyPrefix + ":dest"; + + await client.StringSetAsync(sourceKey, "not_a_geo_key"); + + var position = new GeoPosition(13.361389, 38.115556); + var shape = new GeoSearchCircle(100, GeoUnit.Kilometers); + await Assert.ThrowsAsync(async () => + await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, position, shape)); + } } \ No newline at end of file From 1259a11e76831ac9a45f0b0acda7c45d345f3dff Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Mon, 13 Oct 2025 11:03:43 -0700 Subject: [PATCH 06/27] Add GEOADD options (NX, XX, CH) Signed-off-by: Alex Rehnby-Martin --- .../BaseClient.GeospatialCommands.cs | 14 ++++ .../Commands/IGeospatialCommands.cs | 22 ++++++ sources/Valkey.Glide/ConditionalChange.cs | 19 +++++ sources/Valkey.Glide/GeoAddOptions.cs | 50 +++++++++++++ .../Internals/Request.GeospatialCommands.cs | 63 ++++++++++++++++ .../Pipeline/BaseBatch.GeospatialCommands.cs | 8 ++ .../Pipeline/IBatchGeospatialCommands.cs | 8 ++ .../GeospatialCommandTests.cs | 74 +++++++++++++++++++ 8 files changed, 258 insertions(+) create mode 100644 sources/Valkey.Glide/ConditionalChange.cs create mode 100644 sources/Valkey.Glide/GeoAddOptions.cs diff --git a/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs b/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs index 97403ed1..7836d1d5 100644 --- a/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs +++ b/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs @@ -27,6 +27,20 @@ public async Task GeoAddAsync(ValkeyKey key, GeoEntry[] values, CommandFla return await Command(Request.GeoAddAsync(key, values)); } + /// + public async Task GeoAddAsync(ValkeyKey key, GeoEntry value, GeoAddOptions options, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.GeoAddAsync(key, value, options)); + } + + /// + public async Task GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoAddOptions options, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.GeoAddAsync(key, values, options)); + } + /// public async Task GeoDistanceAsync(ValkeyKey key, ValkeyValue member1, ValkeyValue member2, GeoUnit unit = GeoUnit.Meters, CommandFlags flags = CommandFlags.None) { diff --git a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs index ff1d9482..f98139cf 100644 --- a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs +++ b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs @@ -71,6 +71,28 @@ public interface IGeospatialCommands /// Task GeoAddAsync(ValkeyKey key, GeoEntry[] values, CommandFlags flags = CommandFlags.None); + /// + /// Adds the specified geospatial item to the specified key with options. + /// + /// + /// The key of the sorted set. + /// The geospatial item to add. + /// The options for the GEOADD command. + /// The flags to use for this operation. Currently flags are ignored. + /// The number of elements added or changed, depending on options. + Task GeoAddAsync(ValkeyKey key, GeoEntry value, GeoAddOptions options, CommandFlags flags = CommandFlags.None); + + /// + /// Adds the specified geospatial items to the specified key with options. + /// + /// + /// The key of the sorted set. + /// The geospatial items to add. + /// The options for the GEOADD command. + /// The flags to use for this operation. Currently flags are ignored. + /// The number of elements added or changed, depending on options. + Task GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoAddOptions options, CommandFlags flags = CommandFlags.None); + /// /// Returns the distance between two members in the geospatial index represented by the sorted set. /// diff --git a/sources/Valkey.Glide/ConditionalChange.cs b/sources/Valkey.Glide/ConditionalChange.cs new file mode 100644 index 00000000..9b79df06 --- /dev/null +++ b/sources/Valkey.Glide/ConditionalChange.cs @@ -0,0 +1,19 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +namespace Valkey.Glide; + +/// +/// Conditional change options for commands that support conditional updates. +/// +public enum ConditionalChange +{ + /// + /// Only add new elements. Don't update already existing elements. + /// + ONLY_IF_DOES_NOT_EXIST, + + /// + /// Only update elements that already exist. Don't add new elements. + /// + ONLY_IF_EXISTS +} \ No newline at end of file diff --git a/sources/Valkey.Glide/GeoAddOptions.cs b/sources/Valkey.Glide/GeoAddOptions.cs new file mode 100644 index 00000000..adaad028 --- /dev/null +++ b/sources/Valkey.Glide/GeoAddOptions.cs @@ -0,0 +1,50 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +namespace Valkey.Glide; + +/// +/// Options for the GEOADD command. +/// +public class GeoAddOptions +{ + /// + /// Gets the conditional change option. + /// + public ConditionalChange? ConditionalChange { get; } + + /// + /// Gets whether to return the count of changed elements instead of added elements. + /// + public bool Changed { get; } + + /// + /// Initializes a new instance of the class with conditional change option. + /// + /// The conditional change option. + public GeoAddOptions(ConditionalChange conditionalChange) + { + ConditionalChange = conditionalChange; + Changed = false; + } + + /// + /// Initializes a new instance of the class with changed option. + /// + /// Whether to return the count of changed elements. + public GeoAddOptions(bool changed) + { + ConditionalChange = null; + Changed = changed; + } + + /// + /// Initializes a new instance of the class with both options. + /// + /// The conditional change option. + /// Whether to return the count of changed elements. + public GeoAddOptions(ConditionalChange conditionalChange, bool changed) + { + ConditionalChange = conditionalChange; + Changed = changed; + } +} \ No newline at end of file diff --git a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs index 0ef208e4..2cb29124 100644 --- a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs @@ -38,6 +38,69 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values) return Simple(RequestType.GeoAdd, [.. args]); } + /// + /// Creates a request for GEOADD command with a single value and options. + /// + /// The key of the sorted set. + /// The geospatial item to add. + /// The options for the GEOADD command. + /// A with the request. + public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry value, GeoAddOptions options) + { + List args = [key.ToGlideString()]; + + if (options.ConditionalChange.HasValue) + { + args.Add(options.ConditionalChange.Value == ConditionalChange.ONLY_IF_DOES_NOT_EXIST + ? ValkeyLiterals.NX.ToGlideString() + : ValkeyLiterals.XX.ToGlideString()); + } + + if (options.Changed) + { + args.Add(ValkeyLiterals.CH.ToGlideString()); + } + + args.Add(value.Longitude.ToGlideString()); + args.Add(value.Latitude.ToGlideString()); + args.Add(value.Member.ToGlideString()); + + return Simple(RequestType.GeoAdd, [.. args]); + } + + /// + /// Creates a request for GEOADD command with multiple values and options. + /// + /// The key of the sorted set. + /// The geospatial items to add. + /// The options for the GEOADD command. + /// A with the request. + public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoAddOptions options) + { + List args = [key.ToGlideString()]; + + if (options.ConditionalChange.HasValue) + { + args.Add(options.ConditionalChange.Value == ConditionalChange.ONLY_IF_DOES_NOT_EXIST + ? ValkeyLiterals.NX.ToGlideString() + : ValkeyLiterals.XX.ToGlideString()); + } + + if (options.Changed) + { + args.Add(ValkeyLiterals.CH.ToGlideString()); + } + + foreach (var value in values) + { + args.Add(value.Longitude.ToGlideString()); + args.Add(value.Latitude.ToGlideString()); + args.Add(value.Member.ToGlideString()); + } + + return Simple(RequestType.GeoAdd, [.. args]); + } + /// /// Creates a request for GEODIST command. /// diff --git a/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs b/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs index 1bf827a2..2fbfb40f 100644 --- a/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs +++ b/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs @@ -18,6 +18,12 @@ public abstract partial class BaseBatch /// public T GeoAdd(ValkeyKey key, GeoEntry[] values) => AddCmd(GeoAddAsync(key, values)); + /// + public T GeoAdd(ValkeyKey key, GeoEntry value, GeoAddOptions options) => AddCmd(GeoAddAsync(key, value, options)); + + /// + public T GeoAdd(ValkeyKey key, GeoEntry[] values, GeoAddOptions options) => AddCmd(GeoAddAsync(key, values, options)); + /// public T GeoDistance(ValkeyKey key, ValkeyValue member1, ValkeyValue member2, GeoUnit unit = GeoUnit.Meters) => AddCmd(GeoDistanceAsync(key, member1, member2, unit)); @@ -46,6 +52,8 @@ public abstract partial class BaseBatch IBatch IBatchGeospatialCommands.GeoAdd(ValkeyKey key, double longitude, double latitude, ValkeyValue member) => GeoAdd(key, longitude, latitude, member); IBatch IBatchGeospatialCommands.GeoAdd(ValkeyKey key, GeoEntry value) => GeoAdd(key, value); IBatch IBatchGeospatialCommands.GeoAdd(ValkeyKey key, GeoEntry[] values) => GeoAdd(key, values); + IBatch IBatchGeospatialCommands.GeoAdd(ValkeyKey key, GeoEntry value, GeoAddOptions options) => GeoAdd(key, value, options); + IBatch IBatchGeospatialCommands.GeoAdd(ValkeyKey key, GeoEntry[] values, GeoAddOptions options) => GeoAdd(key, values, options); IBatch IBatchGeospatialCommands.GeoDistance(ValkeyKey key, ValkeyValue member1, ValkeyValue member2, GeoUnit unit) => GeoDistance(key, member1, member2, unit); IBatch IBatchGeospatialCommands.GeoHash(ValkeyKey key, ValkeyValue member) => GeoHash(key, member); IBatch IBatchGeospatialCommands.GeoHash(ValkeyKey key, ValkeyValue[] members) => GeoHash(key, members); diff --git a/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs b/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs index b3f50aed..69d95d40 100644 --- a/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs +++ b/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs @@ -18,6 +18,14 @@ internal interface IBatchGeospatialCommands /// Command Response - IBatch GeoAdd(ValkeyKey key, GeoEntry[] values); + /// + /// Command Response - + IBatch GeoAdd(ValkeyKey key, GeoEntry value, GeoAddOptions options); + + /// + /// Command Response - + IBatch GeoAdd(ValkeyKey key, GeoEntry[] values, GeoAddOptions options); + /// /// Command Response - IBatch GeoDistance(ValkeyKey key, ValkeyValue member1, ValkeyValue member2, GeoUnit unit = GeoUnit.Meters); diff --git a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs index dffb60bc..7d0882c3 100644 --- a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs @@ -115,6 +115,80 @@ public async Task GeoAdd_WrongKeyType_ThrowsException(BaseClient client) await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo")); } + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoAdd_WithNX_OnlyAddsIfNotExists(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Add initial entries + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + long result = await client.GeoAddAsync(key, entries); + Assert.Equal(2, result); + + // Try to add with NX option - should return 0 since members already exist + GeoEntry[] newEntries = + [ + new GeoEntry(13.361389, 39.0, "Palermo"), // Different latitude + new GeoEntry(15.087269, 38.0, "Catania") // Different latitude + ]; + var nxOptions = new GeoAddOptions(ConditionalChange.ONLY_IF_DOES_NOT_EXIST); + long nxResult = await client.GeoAddAsync(key, newEntries, nxOptions); + Assert.Equal(0, nxResult); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoAdd_WithXX_OnlyUpdatesIfExists(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Add initial entries + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + long result = await client.GeoAddAsync(key, entries); + Assert.Equal(2, result); + + // Try to add new member with XX option - should return 0 since member doesn't exist + var newEntry = new GeoEntry(32.0853, 34.7818, "Tel-Aviv"); + var xxOptions = new GeoAddOptions(ConditionalChange.ONLY_IF_EXISTS); + long xxResult = await client.GeoAddAsync(key, newEntry, xxOptions); + Assert.Equal(0, xxResult); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoAdd_WithCH_ReturnsChangedCount(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Add initial entries + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + long result = await client.GeoAddAsync(key, entries); + Assert.Equal(2, result); + + // Update existing member and add new member with CH option + GeoEntry[] updateEntries = + [ + new GeoEntry(15.087269, 40.0, "Catania"), // Update existing + new GeoEntry(32.0853, 34.7818, "Tel-Aviv") // Add new + ]; + var chOptions = new GeoAddOptions(true); // true = CH option + long chResult = await client.GeoAddAsync(key, updateEntries, chOptions); + Assert.Equal(2, chResult); // Should return 2 (1 changed + 1 added) + } + [Theory(DisableDiscoveryEnumeration = true)] [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task GeoDistance_ReturnsCorrectDistance(BaseClient client) From f72b2e459e199cf4eb93fbfa8761d92f70f7e390 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Mon, 13 Oct 2025 11:13:25 -0700 Subject: [PATCH 07/27] Add extensive unit testing (meters, feet, miles), similar to java test Signed-off-by: Alex Rehnby-Martin --- .../GeospatialCommandTests.cs | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs index 7d0882c3..153c1dd8 100644 --- a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs @@ -189,6 +189,109 @@ public async Task GeoAdd_WithCH_ReturnsChangedCount(BaseClient client) Assert.Equal(2, chResult); // Should return 2 (1 changed + 1 added) } + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoDistance_AllUnits_ReturnsCorrectDistances(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + await client.GeoAddAsync(key, entries); + + // Test all units with expected values (approximate distance between Palermo and Catania) + double? distanceMeters = await client.GeoDistanceAsync(key, "Palermo", "Catania", GeoUnit.Meters); + double? distanceKilometers = await client.GeoDistanceAsync(key, "Palermo", "Catania", GeoUnit.Kilometers); + double? distanceMiles = await client.GeoDistanceAsync(key, "Palermo", "Catania", GeoUnit.Miles); + double? distanceFeet = await client.GeoDistanceAsync(key, "Palermo", "Catania", GeoUnit.Feet); + + // Verify all distances are returned + Assert.NotNull(distanceMeters); + Assert.NotNull(distanceKilometers); + Assert.NotNull(distanceMiles); + Assert.NotNull(distanceFeet); + + // Verify approximate expected values (distance between Palermo and Catania) + Assert.True(distanceMeters > 166000 && distanceMeters < 167000); // ~166274 meters + Assert.True(distanceKilometers > 166 && distanceKilometers < 167); // ~166.27 km + Assert.True(distanceMiles > 103 && distanceMiles < 104); // ~103.31 miles + Assert.True(distanceFeet > 545000 && distanceFeet < 546000); // ~545,518 feet + + // Verify unit conversions are consistent + double metersToKm = distanceMeters.Value / 1000; + double metersToMiles = distanceMeters.Value / 1609.344; + double metersToFeet = distanceMeters.Value * 3.28084; + + Assert.True(Math.Abs(metersToKm - distanceKilometers.Value) < 0.001); + Assert.True(Math.Abs(metersToMiles - distanceMiles.Value) < 0.001); + Assert.True(Math.Abs(metersToFeet - distanceFeet.Value) < 1); // Allow 1 foot tolerance + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoDistance_DefaultUnit_ReturnsMeters(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + await client.GeoAddAsync(key, entries); + + // Test default unit (should be meters) + double? distanceDefault = await client.GeoDistanceAsync(key, "Palermo", "Catania"); + double? distanceMeters = await client.GeoDistanceAsync(key, "Palermo", "Catania", GeoUnit.Meters); + + Assert.NotNull(distanceDefault); + Assert.NotNull(distanceMeters); + Assert.Equal(distanceMeters.Value, distanceDefault.Value, 1e-9); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearch_AllUnits_ReturnsConsistentResults(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania"), + new GeoEntry(12.758489, 38.788135, "Trapani") + ]; + await client.GeoAddAsync(key, entries); + + var position = new GeoPosition(13.361389, 38.115556); // Palermo coordinates + + // Test search with different units - all should return same members but with different radius values + var shapeMeters = new GeoSearchCircle(200000, GeoUnit.Meters); // 200km in meters + var shapeKilometers = new GeoSearchCircle(200, GeoUnit.Kilometers); // 200km + var shapeMiles = new GeoSearchCircle(124.27, GeoUnit.Miles); // ~200km in miles + var shapeFeet = new GeoSearchCircle(656168, GeoUnit.Feet); // ~200km in feet + + GeoRadiusResult[] resultsMeters = await client.GeoSearchAsync(key, position, shapeMeters); + GeoRadiusResult[] resultsKilometers = await client.GeoSearchAsync(key, position, shapeKilometers); + GeoRadiusResult[] resultsMiles = await client.GeoSearchAsync(key, position, shapeMiles); + GeoRadiusResult[] resultsFeet = await client.GeoSearchAsync(key, position, shapeFeet); + + // All searches should return the same members (Palermo and Catania should be within 200km) + Assert.NotEmpty(resultsMeters); + Assert.NotEmpty(resultsKilometers); + Assert.NotEmpty(resultsMiles); + Assert.NotEmpty(resultsFeet); + + // Should return same number of results + Assert.Equal(resultsMeters.Length, resultsKilometers.Length); + Assert.Equal(resultsMeters.Length, resultsMiles.Length); + Assert.Equal(resultsMeters.Length, resultsFeet.Length); + + // Should contain Palermo and Catania + Assert.Contains("Palermo", resultsMeters.Select(r => r.Member.ToString())); + Assert.Contains("Catania", resultsMeters.Select(r => r.Member.ToString())); + } + [Theory(DisableDiscoveryEnumeration = true)] [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task GeoDistance_ReturnsCorrectDistance(BaseClient client) From 1a96e418e2649fc55902f6ff0fa62c077853938f Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Mon, 13 Oct 2025 12:00:57 -0700 Subject: [PATCH 08/27] Add edge case tests Signed-off-by: Alex Rehnby-Martin --- .../GeospatialCommandTests.cs | 67 +++++++++++++++++-- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs index 153c1dd8..3b64cc6c 100644 --- a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs @@ -294,7 +294,7 @@ public async Task GeoSearch_AllUnits_ReturnsConsistentResults(BaseClient client) [Theory(DisableDiscoveryEnumeration = true)] [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] - public async Task GeoDistance_ReturnsCorrectDistance(BaseClient client) + public async Task GeoHash_NonExistentMembers_ReturnsNulls(BaseClient client) { string key = Guid.NewGuid().ToString(); GeoEntry[] entries = @@ -302,12 +302,26 @@ public async Task GeoDistance_ReturnsCorrectDistance(BaseClient client) new GeoEntry(13.361389, 38.115556, "Palermo"), new GeoEntry(15.087269, 37.502669, "Catania") ]; - await client.GeoAddAsync(key, entries); - double? distance = await client.GeoDistanceAsync(key, "Palermo", "Catania", GeoUnit.Kilometers); - Assert.NotNull(distance); - Assert.True(distance > 160 && distance < 170); // Approximate distance between Palermo and Catania + string?[] hashes = await client.GeoHashAsync(key, new ValkeyValue[] { "Palermo", "Catania", "NonExistent" }); + Assert.Equal(3, hashes.Length); + Assert.NotNull(hashes[0]); // Palermo + Assert.NotNull(hashes[1]); // Catania + Assert.Null(hashes[2]); // NonExistent + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoPosition_NonExistentMembers_ReturnsNulls(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo"); + + GeoPosition?[] positions = await client.GeoPositionAsync(key, new ValkeyValue[] { "Palermo", "NonExistent" }); + Assert.Equal(2, positions.Length); + Assert.NotNull(positions[0]); // Palermo + Assert.Null(positions[1]); // NonExistent } [Theory(DisableDiscoveryEnumeration = true)] @@ -321,6 +335,49 @@ public async Task GeoDistance_NonExistentMember_ReturnsNull(BaseClient client) Assert.Null(distance); } + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task Geo_NonExistentKey_ReturnsAppropriateDefaults(BaseClient client) + { + string key = Guid.NewGuid().ToString(); // Non-existent key + + // GeoDistance should return null for non-existent key + double? distance = await client.GeoDistanceAsync(key, "member1", "member2"); + Assert.Null(distance); + + // GeoHash should return null for non-existent key + string? hash = await client.GeoHashAsync(key, "member"); + Assert.Null(hash); + + // GeoPosition should return null for non-existent key + GeoPosition? position = await client.GeoPositionAsync(key, "member"); + Assert.Null(position); + + // GeoSearch should return empty array for non-existent key + var searchPosition = new GeoPosition(13.361389, 38.115556); + var shape = new GeoSearchCircle(100, GeoUnit.Kilometers); + GeoRadiusResult[] results = await client.GeoSearchAsync(key, searchPosition, shape); + Assert.Empty(results); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoDistance_ReturnsCorrectDistance(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + + await client.GeoAddAsync(key, entries); + + double? distance = await client.GeoDistanceAsync(key, "Palermo", "Catania", GeoUnit.Kilometers); + Assert.NotNull(distance); + Assert.True(distance > 160 && distance < 170); // Approximate distance between Palermo and Catania + } + [Theory(DisableDiscoveryEnumeration = true)] [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task GeoDistance_WrongKeyType_ThrowsException(BaseClient client) From 07b2b99dc7124051ca85928650153fa7ab79043d Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Mon, 13 Oct 2025 12:58:08 -0700 Subject: [PATCH 09/27] Add comprehensive GEOSEARCHSTORE result verification Signed-off-by: Alex Rehnby-Martin --- .../GeospatialCommandTests.cs | 101 ++++++++++++++++-- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs index 3b64cc6c..12e4377e 100644 --- a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs @@ -785,15 +785,21 @@ public async Task GeoSearchAndStore_WithMember_StoresResults(BaseClient client) GeoEntry[] entries = [ new GeoEntry(13.361389, 38.115556, "Palermo"), - new GeoEntry(15.087269, 37.502669, "Catania") + new GeoEntry(15.087269, 37.502669, "Catania"), + new GeoEntry(12.758489, 38.788135, "Trapani") ]; await client.GeoAddAsync(sourceKey, entries); var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); long count = await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, "Palermo", shape); - Assert.True(count >= 1); - Assert.Contains("Palermo", (await client.SortedSetRangeByRankAsync(destinationKey, 0, -1)).Select(r => r.ToString())); + Assert.Equal(3, count); + + ValkeyValue[] storedMembers = await client.SortedSetRangeByRankAsync(destinationKey, 0, -1); + Assert.Equal(3, storedMembers.Length); + Assert.Contains("Palermo", storedMembers.Select(r => r.ToString())); + Assert.Contains("Catania", storedMembers.Select(r => r.ToString())); + Assert.Contains("Trapani", storedMembers.Select(r => r.ToString())); } [Theory(DisableDiscoveryEnumeration = true)] @@ -837,10 +843,18 @@ public async Task GeoSearchAndStore_WithDistances_StoresDistances(BaseClient cli var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); long count = await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, "Palermo", shape, storeDistances: true); - Assert.True(count >= 1); + Assert.Equal(2, count); + var results = await client.SortedSetRangeByRankWithScoresAsync(destinationKey, 0, -1); - Assert.NotEmpty(results); - Assert.True(results.Any(r => r.Score >= 0)); // Distances should be non-negative + Assert.Equal(2, results.Length); + + var palermoResult = results.FirstOrDefault(r => r.Key.ToString() == "Palermo"); + var cataniaResult = results.FirstOrDefault(r => r.Key.ToString() == "Catania"); + + Assert.NotNull(palermoResult); + Assert.NotNull(cataniaResult); + Assert.Equal(0.0, palermoResult.Value, 0.1); + Assert.Equal(166.2742, cataniaResult.Value, 0.1); } [Theory(DisableDiscoveryEnumeration = true)] @@ -912,4 +926,79 @@ public async Task GeoSearchAndStore_WrongKeyType_ThrowsException(BaseClient clie await Assert.ThrowsAsync(async () => await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, position, shape)); } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearchAndStore_WithCount_LimitsStoredResults(BaseClient client) + { + string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; + string sourceKey = keyPrefix + ":source"; + string destinationKey = keyPrefix + ":dest"; + + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania"), + new GeoEntry(12.758489, 38.788135, "Trapani"), + new GeoEntry(14.015482, 37.734741, "Enna") + ]; + await client.GeoAddAsync(sourceKey, entries); + + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); + long count = await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, "Palermo", shape, count: 2); + + Assert.Equal(2, count); + + ValkeyValue[] storedMembers = await client.SortedSetRangeByRankAsync(destinationKey, 0, -1); + Assert.Equal(2, storedMembers.Length); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearchAndStore_OverwritesDestination(BaseClient client) + { + string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; + string sourceKey = keyPrefix + ":source"; + string destinationKey = keyPrefix + ":dest"; + + GeoEntry[] entries = + [ + new GeoEntry(13.361389, 38.115556, "Palermo"), + new GeoEntry(15.087269, 37.502669, "Catania") + ]; + await client.GeoAddAsync(sourceKey, entries); + + await client.SortedSetAddAsync(destinationKey, new SortedSetEntry[] { new SortedSetEntry("OldMember", 100) }); + Assert.Equal(1, await client.SortedSetCardAsync(destinationKey)); + + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); + long count = await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, "Palermo", shape); + + Assert.Equal(2, count); + + ValkeyValue[] storedMembers = await client.SortedSetRangeByRankAsync(destinationKey, 0, -1); + Assert.Equal(2, storedMembers.Length); + Assert.DoesNotContain("OldMember", storedMembers.Select(m => m.ToString())); + Assert.Contains("Palermo", storedMembers.Select(m => m.ToString())); + Assert.Contains("Catania", storedMembers.Select(m => m.ToString())); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GeoSearchAndStore_NoResults_CreatesEmptyDestination(BaseClient client) + { + string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; + string sourceKey = keyPrefix + ":source"; + string destinationKey = keyPrefix + ":dest"; + + await client.GeoAddAsync(sourceKey, 13.361389, 38.115556, "Palermo"); + + var position = new GeoPosition(0.0, 0.0); + var shape = new GeoSearchCircle(1, GeoUnit.Meters); + long count = await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, position, shape); + + Assert.Equal(0, count); + Assert.Equal(0, await client.SortedSetCardAsync(destinationKey)); + // Verify destination key exists but is empty - TypeAsync not available in BaseClient + } } \ No newline at end of file From 668acfb53471b496566441254b04fd89b830c785 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Mon, 13 Oct 2025 14:03:08 -0700 Subject: [PATCH 10/27] Unit tests Signed-off-by: Alex Rehnby-Martin --- tests/Valkey.Glide.UnitTests/CommandTests.cs | 22 +++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/Valkey.Glide.UnitTests/CommandTests.cs b/tests/Valkey.Glide.UnitTests/CommandTests.cs index dd081401..a94a2d0b 100644 --- a/tests/Valkey.Glide.UnitTests/CommandTests.cs +++ b/tests/Valkey.Glide.UnitTests/CommandTests.cs @@ -282,7 +282,27 @@ public void ValidateCommandArgs() () => Assert.Equal(new string[] { "HVALS", "key" }, Request.HashValuesAsync("key").GetArgs()), () => Assert.Equal(new string[] { "HRANDFIELD", "key" }, Request.HashRandomFieldAsync("key").GetArgs()), () => Assert.Equal(new string[] { "HRANDFIELD", "key", "3" }, Request.HashRandomFieldsAsync("key", 3).GetArgs()), - () => Assert.Equal(new string[] { "HRANDFIELD", "key", "3", "WITHVALUES" }, Request.HashRandomFieldsWithValuesAsync("key", 3).GetArgs()) + () => Assert.Equal(new string[] { "HRANDFIELD", "key", "3", "WITHVALUES" }, Request.HashRandomFieldsWithValuesAsync("key", 3).GetArgs()), + + // Geospatial Commands + () => Assert.Equal(["GEOADD", "key", "13.361389000000001", "38.115555999999998", "Palermo"], Request.GeoAddAsync("key", new GeoEntry(13.361389, 38.115556, "Palermo")).GetArgs()), + () => Assert.Equal(["GEOADD", "key", "13.361389000000001", "38.115555999999998", "Palermo", "15.087268999999999", "37.502668999999997", "Catania"], Request.GeoAddAsync("key", new GeoEntry[] { new GeoEntry(13.361389, 38.115556, "Palermo"), new GeoEntry(15.087269, 37.502669, "Catania") }).GetArgs()), + () => Assert.Equal(["GEOADD", "key", "NX", "13.361389000000001", "38.115555999999998", "Palermo"], Request.GeoAddAsync("key", new GeoEntry(13.361389, 38.115556, "Palermo"), new GeoAddOptions(ConditionalChange.ONLY_IF_DOES_NOT_EXIST)).GetArgs()), + () => Assert.Equal(["GEOADD", "key", "XX", "13.361389000000001", "38.115555999999998", "Palermo"], Request.GeoAddAsync("key", new GeoEntry(13.361389, 38.115556, "Palermo"), new GeoAddOptions(ConditionalChange.ONLY_IF_EXISTS)).GetArgs()), + () => Assert.Equal(["GEOADD", "key", "CH", "13.361389000000001", "38.115555999999998", "Palermo"], Request.GeoAddAsync("key", new GeoEntry(13.361389, 38.115556, "Palermo"), new GeoAddOptions(true)).GetArgs()), + () => Assert.Equal(["GEODIST", "key", "Palermo", "Catania", "m"], Request.GeoDistanceAsync("key", "Palermo", "Catania", GeoUnit.Meters).GetArgs()), + () => Assert.Equal(["GEODIST", "key", "Palermo", "Catania", "km"], Request.GeoDistanceAsync("key", "Palermo", "Catania", GeoUnit.Kilometers).GetArgs()), + () => Assert.Equal(["GEODIST", "key", "Palermo", "Catania", "mi"], Request.GeoDistanceAsync("key", "Palermo", "Catania", GeoUnit.Miles).GetArgs()), + () => Assert.Equal(["GEODIST", "key", "Palermo", "Catania", "ft"], Request.GeoDistanceAsync("key", "Palermo", "Catania", GeoUnit.Feet).GetArgs()), + () => Assert.Equal(["GEOHASH", "key", "Palermo"], Request.GeoHashAsync("key", "Palermo").GetArgs()), + () => Assert.Equal(["GEOHASH", "key", "Palermo", "Catania"], Request.GeoHashAsync("key", new ValkeyValue[] { "Palermo", "Catania" }).GetArgs()), + () => Assert.Equal(["GEOPOS", "key", "Palermo"], Request.GeoPositionAsync("key", "Palermo").GetArgs()), + () => Assert.Equal(["GEOPOS", "key", "Palermo", "Catania"], Request.GeoPositionAsync("key", new ValkeyValue[] { "Palermo", "Catania" }).GetArgs()), + () => Assert.Equal(["GEOSEARCH", "key", "FROMMEMBER", "Palermo", "BYRADIUS", "100", "km"], Request.GeoSearchAsync("key", "Palermo", new GeoSearchCircle(100, GeoUnit.Kilometers)).GetArgs()), + () => Assert.Equal(["GEOSEARCH", "key", "FROMLONLAT", "13.361389000000001", "38.115555999999998", "BYRADIUS", "200", "m"], Request.GeoSearchAsync("key", new GeoPosition(13.361389, 38.115556), new GeoSearchCircle(200, GeoUnit.Meters)).GetArgs()), + () => Assert.Equal(["GEOSEARCH", "key", "FROMMEMBER", "Palermo", "BYBOX", "300", "400", "km"], Request.GeoSearchAsync("key", "Palermo", new GeoSearchBox(400, 300, GeoUnit.Kilometers)).GetArgs()), + () => Assert.Equal(["GEOSEARCHSTORE", "dest", "key", "FROMMEMBER", "Palermo", "BYRADIUS", "100", "km"], Request.GeoSearchAndStoreAsync("key", "dest", "Palermo", new GeoSearchCircle(100, GeoUnit.Kilometers), -1, true, null, false).GetArgs()), + () => Assert.Equal(["GEOSEARCHSTORE", "dest", "key", "FROMLONLAT", "13.361389000000001", "38.115555999999998", "BYRADIUS", "200", "m", "STOREDIST"], Request.GeoSearchAndStoreAsync("key", "dest", new GeoPosition(13.361389, 38.115556), new GeoSearchCircle(200, GeoUnit.Meters), -1, true, null, true).GetArgs()) ); } From c72d96aa1ba5e4d9f26abc91cb5c3fcfd70f267a Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Mon, 13 Oct 2025 15:30:20 -0700 Subject: [PATCH 11/27] Add batch tests Signed-off-by: Alex Rehnby-Martin --- .../BatchTestUtils.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs b/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs index ba7e2f86..dbf4a84a 100644 --- a/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs +++ b/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs @@ -1,5 +1,7 @@ // Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 +using Valkey.Glide.Commands.Options; + namespace Valkey.Glide.IntegrationTests; internal class BatchTestUtils @@ -1390,6 +1392,68 @@ public static List CreateHashTest(Pipeline.IBatch batch, bool isAtomic return testData; } + public static List CreateGeospatialTest(Pipeline.IBatch batch, bool isAtomic) + { + List testData = []; + string prefix = "{geoKey}-"; + string atomicPrefix = isAtomic ? prefix : ""; + string key1 = $"{atomicPrefix}1-{Guid.NewGuid()}"; + string key2 = $"{atomicPrefix}2-{Guid.NewGuid()}"; + string destKey = $"{atomicPrefix}dest-{Guid.NewGuid()}"; + + // Test GeoAdd + _ = batch.GeoAdd(key1, new GeoEntry(13.361389, 38.115556, "Palermo")); + testData.Add(new(1L, "GeoAdd(key1, Palermo)")); + + _ = batch.GeoAdd(key1, new GeoEntry[] { + new GeoEntry(15.087269, 37.502669, "Catania"), + new GeoEntry(12.496366, 41.902782, "Rome") + }); + testData.Add(new(2L, "GeoAdd(key1, [Catania, Rome])")); + + // Test GeoAdd with options + _ = batch.GeoAdd(key1, new GeoEntry(13.361389, 38.115556, "Palermo"), new GeoAddOptions(ConditionalChange.ONLY_IF_EXISTS)); + testData.Add(new(0L, "GeoAdd(key1, Palermo, XX) - update existing")); + + _ = batch.GeoAdd(key1, new GeoEntry(9.189982, 45.4642035, "Milan"), new GeoAddOptions(ConditionalChange.ONLY_IF_DOES_NOT_EXIST)); + testData.Add(new(1L, "GeoAdd(key1, Milan, NX) - add new")); + + // Test GeoDistance + _ = batch.GeoDistance(key1, "Palermo", "Catania", GeoUnit.Kilometers); + testData.Add(new(166.2742, "GeoDistance(key1, Palermo, Catania, km)", true)); + + _ = batch.GeoDistance(key1, "Palermo", "Catania", GeoUnit.Meters); + testData.Add(new(166274.0, "GeoDistance(key1, Palermo, Catania, m)", true)); + + _ = batch.GeoDistance(key1, "Palermo", "NonExistent", GeoUnit.Kilometers); + testData.Add(new(null, "GeoDistance(key1, Palermo, NonExistent, km)")); + + // Test GeoHash - batch returns string for single member, string[] for multiple + _ = batch.GeoHash(key1, "Palermo"); + testData.Add(new("", "GeoHash(key1, Palermo)", true)); + + _ = batch.GeoHash(key1, new ValkeyValue[] { "Palermo", "Catania" }); + testData.Add(new(Array.Empty(), "GeoHash(key1, [Palermo, Catania])", true)); + + // Test GeoPosition + _ = batch.GeoPosition(key1, "Palermo"); + testData.Add(new(new GeoPosition(13.361389, 38.115556), "GeoPosition(key1, Palermo)", true)); + + _ = batch.GeoPosition(key1, new ValkeyValue[] { "Palermo", "NonExistent" }); + testData.Add(new(Array.Empty(), "GeoPosition(key1, [Palermo, NonExistent])", true)); + + // Test GeoSearch + _ = batch.GeoSearch(key1, "Palermo", new GeoSearchCircle(200, GeoUnit.Kilometers)); + testData.Add(new(Array.Empty(), "GeoSearch(key1, Palermo, 200km circle)", true)); + + _ = batch.GeoSearch(key1, new GeoPosition(15, 37), new GeoSearchBox(400, 400, GeoUnit.Kilometers)); + testData.Add(new(Array.Empty(), "GeoSearch(key1, position, 400x400km box)", true)); + + // Note: GeoSearchAndStore is not available in batch interface + + return testData; + } + public static TheoryData GetTestClientWithAtomic => [.. TestConfiguration.TestClients.SelectMany(r => new[] { true, false }.SelectMany(isAtomic => new BatchTestData[] { @@ -1399,6 +1463,7 @@ [.. TestConfiguration.TestClients.SelectMany(r => new[] { true, false }.SelectMa new("Hash commands", r.Data, CreateHashTest, isAtomic), new("List commands", r.Data, CreateListTest, isAtomic), new("Sorted Set commands", r.Data, CreateSortedSetTest, isAtomic), + new("Geospatial commands", r.Data, CreateGeospatialTest, isAtomic), new("Connection Management commands", r.Data, CreateConnectionManagementTest, isAtomic), new("Server Management commands", r.Data, CreateServerManagementTest, isAtomic) }))]; From 5276ba531c9ae8b065ef2b2a0525d0f0a5ae06e0 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Tue, 14 Oct 2025 11:02:51 -0700 Subject: [PATCH 12/27] Remove BYPOLYGON support Signed-off-by: Alex Rehnby-Martin --- .../BaseClient.GeospatialCommands.cs | 7 +-- .../Commands/IGeospatialCommands.cs | 22 +--------- .../Internals/Request.GeospatialCommands.cs | 44 ------------------- .../Pipeline/BaseBatch.GeospatialCommands.cs | 5 +-- .../Pipeline/IBatchGeospatialCommands.cs | 4 +- .../abstract_APITypes/GeoSearchShape.cs | 28 ------------ .../abstract_APITypes/ValkeyLiterals.cs | 2 +- .../GeospatialCommandTests.cs | 32 +------------- 8 files changed, 7 insertions(+), 137 deletions(-) diff --git a/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs b/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs index 7836d1d5..94880ad5 100644 --- a/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs +++ b/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs @@ -90,12 +90,7 @@ public async Task GeoSearchAsync(ValkeyKey key, GeoPosition f return await Command(Request.GeoSearchAsync(key, fromPosition, shape, count, demandClosest, order, options)); } - /// - public async Task GeoSearchAsync(ValkeyKey key, GeoSearchPolygon polygon, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) - { - Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); - return await Command(Request.GeoSearchAsync(key, polygon, count, demandClosest, order, options)); - } + /// public async Task GeoSearchAndStoreAsync(ValkeyKey sourceKey, ValkeyKey destinationKey, ValkeyValue fromMember, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) diff --git a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs index f98139cf..32e36d99 100644 --- a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs +++ b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs @@ -227,27 +227,7 @@ public interface IGeospatialCommands /// Task GeoSearchAsync(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); - /// - /// Returns the members of a geospatial index which are within the borders of the specified polygon. - /// - /// - /// The key of the sorted set. - /// The polygon area to search within. - /// The maximum number of results to return. Use -1 for no limit. - /// When true, returns the closest results. When false, allows any results. - /// The order in which to return results. Null for default ordering. - /// The options for the search result format. - /// The flags to use for this operation. Currently flags are ignored. - /// An array of results within the specified polygon. - /// - /// - /// - /// var polygon = new GeoSearchPolygon(new GeoPosition[] { new(12.0, 37.0), new(16.0, 37.0), new(16.0, 39.0), new(12.0, 39.0) }); - /// GeoRadiusResult[] results = await client.GeoSearchAsync("mygeo", polygon); - /// - /// - /// - Task GeoSearchAsync(ValkeyKey key, GeoSearchPolygon polygon, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); + Task GeoSearchAndStoreAsync(ValkeyKey sourceKey, ValkeyKey destinationKey, ValkeyValue fromMember, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None); diff --git a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs index 2cb29124..498b9d55 100644 --- a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs @@ -274,51 +274,7 @@ public static Cmd GeoSearchAsync(ValkeyKey key, Geo return new(RequestType.GeoSearch, [.. args], false, response => ProcessGeoSearchResponse(response, options)); } - /// - /// Creates a request for GEOSEARCH command with polygon. - /// - /// The key of the sorted set. - /// The polygon to search within. - /// A with the request. - public static Cmd GeoSearchAsync(ValkeyKey key, GeoSearchPolygon polygon) - { - return GeoSearchAsync(key, polygon, -1, true, null, GeoRadiusOptions.None); - } - /// - /// Creates a request for GEOSEARCH command with polygon, count limit, demandClosest option, order, and options. - /// - /// The key of the sorted set. - /// The polygon to search within. - /// The maximum number of results to return. - /// When true, returns the closest results. When false, allows any results. - /// The order in which to return results. - /// The options for the search result format. - /// A with the request. - public static Cmd GeoSearchAsync(ValkeyKey key, GeoSearchPolygon polygon, long count, bool demandClosest, Order? order, GeoRadiusOptions options) - { - List args = [key.ToGlideString()]; - List polygonArgs = []; - polygon.AddArgs(polygonArgs); - args.AddRange(polygonArgs.Select(a => a.ToGlideString())); - if (count > 0) - { - args.Add(ValkeyLiterals.COUNT.ToGlideString()); - args.Add(count.ToGlideString()); - if (!demandClosest) - { - args.Add(ValkeyLiterals.ANY.ToGlideString()); - } - } - if (order.HasValue) - { - args.Add(order.Value.ToLiteral().ToGlideString()); - } - List optionArgs = []; - options.AddArgs(optionArgs); - args.AddRange(optionArgs.Select(a => a.ToGlideString())); - return new(RequestType.GeoSearch, [.. args], false, response => ProcessGeoSearchResponse(response, options)); - } /// /// Processes the response from GEOSEARCH command based on the options. diff --git a/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs b/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs index 2fbfb40f..576ee836 100644 --- a/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs +++ b/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs @@ -45,8 +45,7 @@ public abstract partial class BaseBatch /// public T GeoSearch(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default) => AddCmd(GeoSearchAsync(key, fromPosition, shape, count, demandClosest, order, options)); - /// - public T GeoSearch(ValkeyKey key, GeoSearchPolygon polygon, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default) => AddCmd(GeoSearchAsync(key, polygon, count, demandClosest, order, options)); + // Explicit interface implementations for IBatchGeospatialCommands IBatch IBatchGeospatialCommands.GeoAdd(ValkeyKey key, double longitude, double latitude, ValkeyValue member) => GeoAdd(key, longitude, latitude, member); @@ -61,5 +60,5 @@ public abstract partial class BaseBatch IBatch IBatchGeospatialCommands.GeoPosition(ValkeyKey key, ValkeyValue[] members) => GeoPosition(key, members); IBatch IBatchGeospatialCommands.GeoSearch(ValkeyKey key, ValkeyValue fromMember, GeoSearchShape shape, long count, bool demandClosest, Order? order, GeoRadiusOptions options) => GeoSearch(key, fromMember, shape, count, demandClosest, order, options); IBatch IBatchGeospatialCommands.GeoSearch(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape, long count, bool demandClosest, Order? order, GeoRadiusOptions options) => GeoSearch(key, fromPosition, shape, count, demandClosest, order, options); - IBatch IBatchGeospatialCommands.GeoSearch(ValkeyKey key, GeoSearchPolygon polygon, long count, bool demandClosest, Order? order, GeoRadiusOptions options) => GeoSearch(key, polygon, count, demandClosest, order, options); + } \ No newline at end of file diff --git a/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs b/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs index 69d95d40..ad058a0a 100644 --- a/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs +++ b/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs @@ -54,7 +54,5 @@ internal interface IBatchGeospatialCommands /// Command Response - IBatch GeoSearch(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default); - /// - /// Command Response - - IBatch GeoSearch(ValkeyKey key, GeoSearchPolygon polygon, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default); + } \ No newline at end of file diff --git a/sources/Valkey.Glide/abstract_APITypes/GeoSearchShape.cs b/sources/Valkey.Glide/abstract_APITypes/GeoSearchShape.cs index ebafa8f0..66ed3d0b 100644 --- a/sources/Valkey.Glide/abstract_APITypes/GeoSearchShape.cs +++ b/sources/Valkey.Glide/abstract_APITypes/GeoSearchShape.cs @@ -91,32 +91,4 @@ internal override void AddArgs(List args) } } -/// -/// A polygon drawn on a map. -/// -public class GeoSearchPolygon : GeoSearchShape -{ - private readonly GeoPosition[] _vertices; - /// - /// Initializes a GeoPolygon. - /// - /// The vertices of the polygon. - public GeoSearchPolygon(GeoPosition[] vertices) : base(GeoUnit.Meters) - { - _vertices = vertices; - } - - internal override int ArgCount => 2 + (_vertices.Length * 2); - - internal override void AddArgs(List args) - { - args.Add(ValkeyLiterals.BYPOLYGON); - args.Add(_vertices.Length); - foreach (var vertex in _vertices) - { - args.Add(vertex.Longitude); - args.Add(vertex.Latitude); - } - } -} diff --git a/sources/Valkey.Glide/abstract_APITypes/ValkeyLiterals.cs b/sources/Valkey.Glide/abstract_APITypes/ValkeyLiterals.cs index 631dfd0b..add29396 100644 --- a/sources/Valkey.Glide/abstract_APITypes/ValkeyLiterals.cs +++ b/sources/Valkey.Glide/abstract_APITypes/ValkeyLiterals.cs @@ -136,7 +136,7 @@ public static readonly ValkeyValue // Geo Radius/Search Literals BYBOX = "BYBOX", - BYPOLYGON = "BYPOLYGON", + BYRADIUS = "BYRADIUS", FROMMEMBER = "FROMMEMBER", FROMLONLAT = "FROMLONLAT", diff --git a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs index 12e4377e..12ff351c 100644 --- a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs @@ -597,37 +597,7 @@ public async Task GeoSearch_FromPosition_ByBox_ReturnsMembers(BaseClient client) Assert.Contains("Catania", results.Select(r => r.Member.ToString())); } - [Theory(DisableDiscoveryEnumeration = true)] - [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] - public async Task GeoSearch_ByPolygon_ReturnsMembers(BaseClient client) - { - Assert.SkipWhen( - TestConfiguration.SERVER_VERSION < new Version(9, 0, 0), - "BYPOLYGON is only supported in Valkey 9.0.0+" - ); - - string key = Guid.NewGuid().ToString(); - GeoEntry[] entries = - [ - new GeoEntry(13.361389, 38.115556, "Palermo"), - new GeoEntry(15.087269, 37.502669, "Catania") - ]; - await client.GeoAddAsync(key, entries); - - var vertices = new GeoPosition[] - { - new GeoPosition(12.0, 37.0), - new GeoPosition(16.0, 37.0), - new GeoPosition(16.0, 39.0), - new GeoPosition(12.0, 39.0) - }; - var polygon = new GeoSearchPolygon(vertices); - GeoRadiusResult[] results = await client.GeoSearchAsync(key, polygon); - - Assert.NotEmpty(results); - Assert.Contains("Palermo", results.Select(r => r.Member.ToString())); - Assert.Contains("Catania", results.Select(r => r.Member.ToString())); - } + [Theory(DisableDiscoveryEnumeration = true)] [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] From 4dfb7e4ef9ca8112935a917737bab0a1595996a8 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Wed, 15 Oct 2025 09:19:19 -0700 Subject: [PATCH 13/27] Cleanup Signed-off-by: Alex Rehnby-Martin --- .../BaseClient.GeospatialCommands.cs | 2 - .../Commands/IGeospatialCommands.cs | 46 ++++++++++++++++++- .../abstract_APITypes/GeoSearchShape.cs | 2 - .../abstract_APITypes/ValkeyLiterals.cs | 1 - 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs b/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs index 94880ad5..0fc1348a 100644 --- a/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs +++ b/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs @@ -90,8 +90,6 @@ public async Task GeoSearchAsync(ValkeyKey key, GeoPosition f return await Command(Request.GeoSearchAsync(key, fromPosition, shape, count, demandClosest, order, options)); } - - /// public async Task GeoSearchAndStoreAsync(ValkeyKey sourceKey, ValkeyKey destinationKey, ValkeyValue fromMember, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) { diff --git a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs index 32e36d99..127d24ba 100644 --- a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs +++ b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs @@ -228,8 +228,52 @@ public interface IGeospatialCommands Task GeoSearchAsync(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); - + /// + /// Searches for members in a geospatial index and stores the results in a destination key. + /// + /// + /// The key of the source sorted set. + /// The key where results will be stored. + /// The member to search from. + /// The search area shape. + /// The maximum number of results to store. Use -1 for no limit. + /// When true, stores the closest results. When false, allows any results. + /// The order in which to store results. Null for default ordering. + /// When true, stores distances as scores. When false, stores geohash values as scores. + /// The flags to use for this operation. Currently flags are ignored. + /// The number of elements stored in the destination key. + /// + /// + /// + /// var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); + /// long count = await client.GeoSearchAndStoreAsync("source", "dest", "Palermo", shape); + /// + /// + /// Task GeoSearchAndStoreAsync(ValkeyKey sourceKey, ValkeyKey destinationKey, ValkeyValue fromMember, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None); + /// + /// Searches for members in a geospatial index and stores the results in a destination key. + /// + /// + /// The key of the source sorted set. + /// The key where results will be stored. + /// The position to search from. + /// The search area shape. + /// The maximum number of results to store. Use -1 for no limit. + /// When true, stores the closest results. When false, allows any results. + /// The order in which to store results. Null for default ordering. + /// When true, stores distances as scores. When false, stores geohash values as scores. + /// The flags to use for this operation. Currently flags are ignored. + /// The number of elements stored in the destination key. + /// + /// + /// + /// var position = new GeoPosition(15.087269, 37.502669); + /// var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); + /// long count = await client.GeoSearchAndStoreAsync("source", "dest", position, shape); + /// + /// + /// Task GeoSearchAndStoreAsync(ValkeyKey sourceKey, ValkeyKey destinationKey, GeoPosition fromPosition, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None); } \ No newline at end of file diff --git a/sources/Valkey.Glide/abstract_APITypes/GeoSearchShape.cs b/sources/Valkey.Glide/abstract_APITypes/GeoSearchShape.cs index 66ed3d0b..20cb0c7d 100644 --- a/sources/Valkey.Glide/abstract_APITypes/GeoSearchShape.cs +++ b/sources/Valkey.Glide/abstract_APITypes/GeoSearchShape.cs @@ -90,5 +90,3 @@ internal override void AddArgs(List args) args.Add(Unit.ToLiteral()); } } - - diff --git a/sources/Valkey.Glide/abstract_APITypes/ValkeyLiterals.cs b/sources/Valkey.Glide/abstract_APITypes/ValkeyLiterals.cs index add29396..1bb0fea1 100644 --- a/sources/Valkey.Glide/abstract_APITypes/ValkeyLiterals.cs +++ b/sources/Valkey.Glide/abstract_APITypes/ValkeyLiterals.cs @@ -136,7 +136,6 @@ public static readonly ValkeyValue // Geo Radius/Search Literals BYBOX = "BYBOX", - BYRADIUS = "BYRADIUS", FROMMEMBER = "FROMMEMBER", FROMLONLAT = "FROMLONLAT", From 2133d68db89405287ceaaf362f84216c5ce27f9f Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Fri, 17 Oct 2025 10:26:54 -0700 Subject: [PATCH 14/27] Resolve feedback Signed-off-by: Alex Rehnby-Martin --- .../BaseClient.GeospatialCommands.cs | 4 +- .../Commands/IGeospatialCommands.cs | 4 +- .../Internals/Request.GeospatialCommands.cs | 48 +++++++++---------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs b/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs index 0fc1348a..90571cec 100644 --- a/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs +++ b/sources/Valkey.Glide/BaseClient.GeospatialCommands.cs @@ -28,7 +28,7 @@ public async Task GeoAddAsync(ValkeyKey key, GeoEntry[] values, CommandFla } /// - public async Task GeoAddAsync(ValkeyKey key, GeoEntry value, GeoAddOptions options, CommandFlags flags = CommandFlags.None) + public async Task GeoAddAsync(ValkeyKey key, GeoEntry value, GeoAddOptions options, CommandFlags flags = CommandFlags.None) { Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); return await Command(Request.GeoAddAsync(key, value, options)); @@ -103,4 +103,4 @@ public async Task GeoSearchAndStoreAsync(ValkeyKey sourceKey, ValkeyKey de Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); return await Command(Request.GeoSearchAndStoreAsync(sourceKey, destinationKey, fromPosition, shape, count, demandClosest, order, storeDistances)); } -} \ No newline at end of file +} diff --git a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs index 127d24ba..5f1c469f 100644 --- a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs +++ b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs @@ -79,8 +79,8 @@ public interface IGeospatialCommands /// The geospatial item to add. /// The options for the GEOADD command. /// The flags to use for this operation. Currently flags are ignored. - /// The number of elements added or changed, depending on options. - Task GeoAddAsync(ValkeyKey key, GeoEntry value, GeoAddOptions options, CommandFlags flags = CommandFlags.None); + /// if the element was added or changed, otherwise. + Task GeoAddAsync(ValkeyKey key, GeoEntry value, GeoAddOptions options, CommandFlags flags = CommandFlags.None); /// /// Adds the specified geospatial items to the specified key with options. diff --git a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs index 498b9d55..8c49f30a 100644 --- a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs @@ -27,7 +27,7 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry value) public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values) { List args = [key.ToGlideString()]; - + foreach (var value in values) { args.Add(value.Longitude.ToGlideString()); @@ -45,27 +45,27 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values) /// The geospatial item to add. /// The options for the GEOADD command. /// A with the request. - public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry value, GeoAddOptions options) + public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry value, GeoAddOptions options) { List args = [key.ToGlideString()]; - + if (options.ConditionalChange.HasValue) { - args.Add(options.ConditionalChange.Value == ConditionalChange.ONLY_IF_DOES_NOT_EXIST - ? ValkeyLiterals.NX.ToGlideString() + args.Add(options.ConditionalChange.Value == ConditionalChange.ONLY_IF_DOES_NOT_EXIST + ? ValkeyLiterals.NX.ToGlideString() : ValkeyLiterals.XX.ToGlideString()); } - + if (options.Changed) { args.Add(ValkeyLiterals.CH.ToGlideString()); } - + args.Add(value.Longitude.ToGlideString()); args.Add(value.Latitude.ToGlideString()); args.Add(value.Member.ToGlideString()); - - return Simple(RequestType.GeoAdd, [.. args]); + + return Boolean(RequestType.GeoAdd, [.. args]); } /// @@ -78,19 +78,19 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry value, GeoAddO public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoAddOptions options) { List args = [key.ToGlideString()]; - + if (options.ConditionalChange.HasValue) { - args.Add(options.ConditionalChange.Value == ConditionalChange.ONLY_IF_DOES_NOT_EXIST - ? ValkeyLiterals.NX.ToGlideString() + args.Add(options.ConditionalChange.Value == ConditionalChange.ONLY_IF_DOES_NOT_EXIST + ? ValkeyLiterals.NX.ToGlideString() : ValkeyLiterals.XX.ToGlideString()); } - + if (options.Changed) { args.Add(ValkeyLiterals.CH.ToGlideString()); } - + foreach (var value in values) { args.Add(value.Longitude.ToGlideString()); @@ -148,7 +148,7 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoA public static Cmd GeoPositionAsync(ValkeyKey key, ValkeyValue member) { GlideString[] args = [key.ToGlideString(), member.ToGlideString()]; - return new(RequestType.GeoPos, args, false, response => + return new(RequestType.GeoPos, args, false, response => { if (response.Length == 0 || response[0] == null) return (GeoPosition?)null; var posArray = (object[])response[0]; @@ -166,9 +166,9 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoA public static Cmd GeoPositionAsync(ValkeyKey key, ValkeyValue[] members) { GlideString[] args = [key.ToGlideString(), .. members.Select(m => m.ToGlideString())]; - return new(RequestType.GeoPos, args, false, response => + return new(RequestType.GeoPos, args, false, response => { - return response.Select(item => + return response.Select(item => { if (item == null) return (GeoPosition?)null; var posArray = (object[])item; @@ -285,7 +285,7 @@ public static Cmd GeoSearchAsync(ValkeyKey key, Geo private static GeoRadiusResult[] ProcessGeoSearchResponse(object[] response, GeoRadiusOptions options) { - + return response.Select(item => { // If no options are specified, Redis returns simple strings (member names) @@ -307,12 +307,12 @@ private static GeoRadiusResult[] ProcessGeoSearchResponse(object[] response, Geo GeoPosition? position = null; int index = 1; - + // Redis returns additional data in this specific order: // 1. Distance (if WITHDIST) - // 2. Hash (if WITHHASH) + // 2. Hash (if WITHHASH) // 3. Coordinates (if WITHCOORD) - + if ((options & GeoRadiusOptions.WithDistance) != 0 && index < itemArray.Length) { // Distance comes as a nested array: [distance_value] @@ -323,7 +323,7 @@ private static GeoRadiusResult[] ProcessGeoSearchResponse(object[] response, Geo } index++; } - + if ((options & GeoRadiusOptions.WithGeoHash) != 0 && index < itemArray.Length) { // Hash comes as a nested array: [hash_value] @@ -334,7 +334,7 @@ private static GeoRadiusResult[] ProcessGeoSearchResponse(object[] response, Geo } index++; } - + if ((options & GeoRadiusOptions.WithCoordinates) != 0 && index < itemArray.Length) { // Coordinates come as a triple-nested array: [[[longitude, latitude]]] @@ -428,4 +428,4 @@ public static Cmd GeoSearchAndStoreAsync(ValkeyKey sourceKey, Valkey } return Simple(RequestType.GeoSearchStore, [.. args]); } -} \ No newline at end of file +} From 3d5a80a2dea0217343901343b973973c47792aa7 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Fri, 17 Oct 2025 10:27:23 -0700 Subject: [PATCH 15/27] Update sources/Valkey.Glide/Commands/IGeospatialCommands.cs Co-authored-by: Taylor Curran Signed-off-by: Alex Rehnby-Martin --- sources/Valkey.Glide/Commands/IGeospatialCommands.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs index 5f1c469f..a7846a13 100644 --- a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs +++ b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs @@ -204,7 +204,7 @@ public interface IGeospatialCommands Task GeoSearchAsync(ValkeyKey key, ValkeyValue fromMember, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); /// - /// Returns the members of a geospatial index which are within the borders of the area specified by a given shape. + /// Returns the members of a geospatial index which are within the borders of the area specified by a given shape, centred on the specified position. /// /// /// The key of the sorted set. From 6e35801d4efdfefb5b8f63c35047ab89dc883a83 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Fri, 17 Oct 2025 10:31:57 -0700 Subject: [PATCH 16/27] fix Signed-off-by: Alex Rehnby-Martin --- sources/Valkey.Glide/Commands/IGeospatialCommands.cs | 3 ++- tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs | 4 ++-- .../Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs index a7846a13..db2ab6d0 100644 --- a/sources/Valkey.Glide/Commands/IGeospatialCommands.cs +++ b/sources/Valkey.Glide/Commands/IGeospatialCommands.cs @@ -182,6 +182,7 @@ public interface IGeospatialCommands /// /// Returns the members of a geospatial index which are within the borders of the area specified by a given shape. + /// The shape is centred on the specified member or longlat. /// /// /// The key of the sorted set. @@ -276,4 +277,4 @@ public interface IGeospatialCommands /// /// Task GeoSearchAndStoreAsync(ValkeyKey sourceKey, ValkeyKey destinationKey, GeoPosition fromPosition, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None); -} \ No newline at end of file +} diff --git a/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs b/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs index dbf4a84a..6b2e4102 100644 --- a/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs +++ b/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs @@ -1413,10 +1413,10 @@ public static List CreateGeospatialTest(Pipeline.IBatch batch, bool is // Test GeoAdd with options _ = batch.GeoAdd(key1, new GeoEntry(13.361389, 38.115556, "Palermo"), new GeoAddOptions(ConditionalChange.ONLY_IF_EXISTS)); - testData.Add(new(0L, "GeoAdd(key1, Palermo, XX) - update existing")); + testData.Add(new(false, "GeoAdd(key1, Palermo, XX) - update existing")); _ = batch.GeoAdd(key1, new GeoEntry(9.189982, 45.4642035, "Milan"), new GeoAddOptions(ConditionalChange.ONLY_IF_DOES_NOT_EXIST)); - testData.Add(new(1L, "GeoAdd(key1, Milan, NX) - add new")); + testData.Add(new(true, "GeoAdd(key1, Milan, NX) - add new")); // Test GeoDistance _ = batch.GeoDistance(key1, "Palermo", "Catania", GeoUnit.Kilometers); diff --git a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs index 12ff351c..43686ea4 100644 --- a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs @@ -156,11 +156,11 @@ public async Task GeoAdd_WithXX_OnlyUpdatesIfExists(BaseClient client) long result = await client.GeoAddAsync(key, entries); Assert.Equal(2, result); - // Try to add new member with XX option - should return 0 since member doesn't exist + // Try to add new member with XX option - should return false since member doesn't exist var newEntry = new GeoEntry(32.0853, 34.7818, "Tel-Aviv"); var xxOptions = new GeoAddOptions(ConditionalChange.ONLY_IF_EXISTS); - long xxResult = await client.GeoAddAsync(key, newEntry, xxOptions); - Assert.Equal(0, xxResult); + bool xxResult = await client.GeoAddAsync(key, newEntry, xxOptions); + Assert.False(xxResult); } [Theory(DisableDiscoveryEnumeration = true)] From dae7a7c1827d52993e0aa55ab747b76ec7beceb6 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Fri, 17 Oct 2025 10:38:05 -0700 Subject: [PATCH 17/27] Update tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs Co-authored-by: Taylor Curran Signed-off-by: Alex Rehnby-Martin --- tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs index 43686ea4..872b959d 100644 --- a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs @@ -245,8 +245,6 @@ public async Task GeoDistance_DefaultUnit_ReturnsMeters(BaseClient client) double? distanceDefault = await client.GeoDistanceAsync(key, "Palermo", "Catania"); double? distanceMeters = await client.GeoDistanceAsync(key, "Palermo", "Catania", GeoUnit.Meters); - Assert.NotNull(distanceDefault); - Assert.NotNull(distanceMeters); Assert.Equal(distanceMeters.Value, distanceDefault.Value, 1e-9); } From 5fed14e86435e42bcacb6676be8a2eab9bc41431 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Fri, 17 Oct 2025 11:32:35 -0700 Subject: [PATCH 18/27] Address PR feedback Signed-off-by: Alex Rehnby-Martin --- .../Internals/Request.GeospatialCommands.cs | 187 ++++--------- .../GeospatialCommandTests.cs | 264 +++++++++--------- 2 files changed, 190 insertions(+), 261 deletions(-) diff --git a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs index 8c49f30a..ca06ec5c 100644 --- a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs @@ -7,97 +7,59 @@ namespace Valkey.Glide.Internals; internal static partial class Request { /// - /// Creates a request for GEOADD command. - /// - /// The key of the sorted set. - /// The geospatial item to add. - /// A with the request. - public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry value) - { - GlideString[] args = [key.ToGlideString(), value.Longitude.ToGlideString(), value.Latitude.ToGlideString(), value.Member.ToGlideString()]; - return Boolean(RequestType.GeoAdd, args); - } - - /// - /// Creates a request for GEOADD command with multiple values. + /// Adds GeoAddOptions to the argument list. /// - /// The key of the sorted set. - /// The geospatial items to add. - /// A with the request. - public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values) + /// The argument list to add options to. + /// The options to add. + private static void AddGeoAddOptions(List args, GeoAddOptions? options) { - List args = [key.ToGlideString()]; - - foreach (var value in values) + if (options?.ConditionalChange.HasValue == true) { - args.Add(value.Longitude.ToGlideString()); - args.Add(value.Latitude.ToGlideString()); - args.Add(value.Member.ToGlideString()); + args.Add(options.ConditionalChange.Value == ConditionalChange.ONLY_IF_DOES_NOT_EXIST + ? ValkeyLiterals.NX.ToGlideString() + : ValkeyLiterals.XX.ToGlideString()); } - return Simple(RequestType.GeoAdd, [.. args]); + if (options?.Changed == true) + { + args.Add(ValkeyLiterals.CH.ToGlideString()); + } } /// - /// Creates a request for GEOADD command with a single value and options. + /// Creates a request for GEOADD command. /// /// The key of the sorted set. /// The geospatial item to add. /// The options for the GEOADD command. /// A with the request. - public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry value, GeoAddOptions options) + public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry value, GeoAddOptions? options = null) { List args = [key.ToGlideString()]; - - if (options.ConditionalChange.HasValue) - { - args.Add(options.ConditionalChange.Value == ConditionalChange.ONLY_IF_DOES_NOT_EXIST - ? ValkeyLiterals.NX.ToGlideString() - : ValkeyLiterals.XX.ToGlideString()); - } - - if (options.Changed) - { - args.Add(ValkeyLiterals.CH.ToGlideString()); - } - + AddGeoAddOptions(args, options); args.Add(value.Longitude.ToGlideString()); args.Add(value.Latitude.ToGlideString()); args.Add(value.Member.ToGlideString()); - return Boolean(RequestType.GeoAdd, [.. args]); } /// - /// Creates a request for GEOADD command with multiple values and options. + /// Creates a request for GEOADD command with multiple values. /// /// The key of the sorted set. /// The geospatial items to add. /// The options for the GEOADD command. /// A with the request. - public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoAddOptions options) + public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoAddOptions? options = null) { List args = [key.ToGlideString()]; - - if (options.ConditionalChange.HasValue) - { - args.Add(options.ConditionalChange.Value == ConditionalChange.ONLY_IF_DOES_NOT_EXIST - ? ValkeyLiterals.NX.ToGlideString() - : ValkeyLiterals.XX.ToGlideString()); - } - - if (options.Changed) - { - args.Add(ValkeyLiterals.CH.ToGlideString()); - } - + AddGeoAddOptions(args, options); foreach (var value in values) { args.Add(value.Longitude.ToGlideString()); args.Add(value.Latitude.ToGlideString()); args.Add(value.Member.ToGlideString()); } - return Simple(RequestType.GeoAdd, [.. args]); } @@ -139,6 +101,19 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoA return new(RequestType.GeoHash, args, false, response => response.Select(item => item?.ToString()).ToArray()); } + /// + /// Parses a position array into a GeoPosition. + /// + /// The position array from server response. + /// A GeoPosition or null if parsing fails. + private static GeoPosition? ParseGeoPosition(object? item) + { + if (item == null) return null; + var posArray = (object[])item; + if (posArray.Length < 2 || posArray[0] == null || posArray[1] == null) return null; + return new GeoPosition(double.Parse(posArray[0].ToString()!), double.Parse(posArray[1].ToString()!)); + } + /// /// Creates a request for GEOPOS command for a single member. /// @@ -148,13 +123,8 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoA public static Cmd GeoPositionAsync(ValkeyKey key, ValkeyValue member) { GlideString[] args = [key.ToGlideString(), member.ToGlideString()]; - return new(RequestType.GeoPos, args, false, response => - { - if (response.Length == 0 || response[0] == null) return (GeoPosition?)null; - var posArray = (object[])response[0]; - if (posArray.Length < 2 || posArray[0] == null || posArray[1] == null) return (GeoPosition?)null; - return (GeoPosition?)new GeoPosition(double.Parse(posArray[0].ToString()!), double.Parse(posArray[1].ToString()!)); - }); + return new(RequestType.GeoPos, args, false, response => + response.Length > 0 ? ParseGeoPosition(response[0]) : null); } /// @@ -166,16 +136,8 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoA public static Cmd GeoPositionAsync(ValkeyKey key, ValkeyValue[] members) { GlideString[] args = [key.ToGlideString(), .. members.Select(m => m.ToGlideString())]; - return new(RequestType.GeoPos, args, false, response => - { - return response.Select(item => - { - if (item == null) return (GeoPosition?)null; - var posArray = (object[])item; - if (posArray.Length < 2 || posArray[0] == null || posArray[1] == null) return (GeoPosition?)null; - return (GeoPosition?)new GeoPosition(double.Parse(posArray[0].ToString()!), double.Parse(posArray[1].ToString()!)); - }).ToArray(); - }); + return new(RequestType.GeoPos, args, false, response => + response.Select(ParseGeoPosition).ToArray()); } /// @@ -184,24 +146,12 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoA /// The key of the sorted set. /// The member to search from. /// The search area shape. - /// A with the request. - public static Cmd GeoSearchAsync(ValkeyKey key, ValkeyValue fromMember, GeoSearchShape shape) - { - return GeoSearchAsync(key, fromMember, shape, -1, true, null, GeoRadiusOptions.None); - } - - /// - /// Creates a request for GEOSEARCH command with member origin, count limit, demandClosest option, order, and options. - /// - /// The key of the sorted set. - /// The member to search from. - /// The search area shape. /// The maximum number of results to return. /// When true, returns the closest results. When false, allows any results. /// The order in which to return results. /// The options for the search result format. /// A with the request. - public static Cmd GeoSearchAsync(ValkeyKey key, ValkeyValue fromMember, GeoSearchShape shape, long count, bool demandClosest, Order? order, GeoRadiusOptions options) + public static Cmd GeoSearchAsync(ValkeyKey key, ValkeyValue fromMember, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.None) { List args = [key.ToGlideString(), ValkeyLiterals.FROMMEMBER.ToGlideString(), fromMember.ToGlideString()]; List shapeArgs = []; @@ -232,24 +182,12 @@ public static Cmd GeoSearchAsync(ValkeyKey key, Val /// The key of the sorted set. /// The position to search from. /// The search area shape. - /// A with the request. - public static Cmd GeoSearchAsync(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape) - { - return GeoSearchAsync(key, fromPosition, shape, -1, true, null, GeoRadiusOptions.None); - } - - /// - /// Creates a request for GEOSEARCH command with position origin, count limit, demandClosest option, order, and options. - /// - /// The key of the sorted set. - /// The position to search from. - /// The search area shape. /// The maximum number of results to return. /// When true, returns the closest results. When false, allows any results. /// The order in which to return results. /// The options for the search result format. /// A with the request. - public static Cmd GeoSearchAsync(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape, long count, bool demandClosest, Order? order, GeoRadiusOptions options) + public static Cmd GeoSearchAsync(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.None) { List args = [key.ToGlideString(), ValkeyLiterals.FROMLONLAT.ToGlideString(), fromPosition.Longitude.ToGlideString(), fromPosition.Latitude.ToGlideString()]; List shapeArgs = []; @@ -354,20 +292,16 @@ private static GeoRadiusResult[] ProcessGeoSearchResponse(object[] response, Geo } /// - /// Creates a request for GEOSEARCHSTORE command with member origin. + /// Adds common GeoSearchAndStore arguments to the argument list. /// - /// The key of the source sorted set. - /// The key where results will be stored. - /// The member to search from. + /// The argument list to add to. /// The search area shape. /// The maximum number of results to return. - /// When true, returns the closest results. When false, allows any results. + /// When true, returns the closest results. /// The order in which to return results. /// When true, stores distances instead of just member names. - /// A with the request. - public static Cmd GeoSearchAndStoreAsync(ValkeyKey sourceKey, ValkeyKey destinationKey, ValkeyValue fromMember, GeoSearchShape shape, long count, bool demandClosest, Order? order, bool storeDistances) + private static void AddGeoSearchAndStoreArgs(List args, GeoSearchShape shape, long count, bool demandClosest, Order? order, bool storeDistances) { - List args = [destinationKey.ToGlideString(), sourceKey.ToGlideString(), ValkeyLiterals.FROMMEMBER.ToGlideString(), fromMember.ToGlideString()]; List shapeArgs = []; shape.AddArgs(shapeArgs); args.AddRange(shapeArgs.Select(a => a.ToGlideString())); @@ -388,6 +322,24 @@ public static Cmd GeoSearchAndStoreAsync(ValkeyKey sourceKey, Valkey { args.Add(ValkeyLiterals.STOREDIST.ToGlideString()); } + } + + /// + /// Creates a request for GEOSEARCHSTORE command with member origin. + /// + /// The key of the source sorted set. + /// The key where results will be stored. + /// The member to search from. + /// The search area shape. + /// The maximum number of results to return. + /// When true, returns the closest results. When false, allows any results. + /// The order in which to return results. + /// When true, stores distances instead of just member names. + /// A with the request. + public static Cmd GeoSearchAndStoreAsync(ValkeyKey sourceKey, ValkeyKey destinationKey, ValkeyValue fromMember, GeoSearchShape shape, long count, bool demandClosest, Order? order, bool storeDistances) + { + List args = [destinationKey.ToGlideString(), sourceKey.ToGlideString(), ValkeyLiterals.FROMMEMBER.ToGlideString(), fromMember.ToGlideString()]; + AddGeoSearchAndStoreArgs(args, shape, count, demandClosest, order, storeDistances); return Simple(RequestType.GeoSearchStore, [.. args]); } @@ -406,26 +358,7 @@ public static Cmd GeoSearchAndStoreAsync(ValkeyKey sourceKey, Valkey public static Cmd GeoSearchAndStoreAsync(ValkeyKey sourceKey, ValkeyKey destinationKey, GeoPosition fromPosition, GeoSearchShape shape, long count, bool demandClosest, Order? order, bool storeDistances) { List args = [destinationKey.ToGlideString(), sourceKey.ToGlideString(), ValkeyLiterals.FROMLONLAT.ToGlideString(), fromPosition.Longitude.ToGlideString(), fromPosition.Latitude.ToGlideString()]; - List shapeArgs = []; - shape.AddArgs(shapeArgs); - args.AddRange(shapeArgs.Select(a => a.ToGlideString())); - if (count > 0) - { - args.Add(ValkeyLiterals.COUNT.ToGlideString()); - args.Add(count.ToGlideString()); - if (!demandClosest) - { - args.Add(ValkeyLiterals.ANY.ToGlideString()); - } - } - if (order.HasValue) - { - args.Add(order.Value.ToLiteral().ToGlideString()); - } - if (storeDistances) - { - args.Add(ValkeyLiterals.STOREDIST.ToGlideString()); - } + AddGeoSearchAndStoreArgs(args, shape, count, demandClosest, order, storeDistances); return Simple(RequestType.GeoSearchStore, [.. args]); } } diff --git a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs index 43686ea4..62c549ae 100644 --- a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs @@ -69,11 +69,11 @@ public async Task GeoAdd_InvalidLongitude_ThrowsException(BaseClient client) string member = "InvalidPlace"; // Test longitude too low (-181) - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await client.GeoAddAsync(key, -181, 0, member)); // Test longitude too high (181) - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await client.GeoAddAsync(key, 181, 0, member)); } @@ -85,11 +85,11 @@ public async Task GeoAdd_InvalidLatitude_ThrowsException(BaseClient client) string member = "InvalidPlace"; // Test latitude too high (86) - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await client.GeoAddAsync(key, 0, 86, member)); // Test latitude too low (-86) - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await client.GeoAddAsync(key, 0, -86, member)); } @@ -100,7 +100,7 @@ public async Task GeoAdd_EmptyEntries_ThrowsException(BaseClient client) string key = Guid.NewGuid().ToString(); GeoEntry[] emptyEntries = []; - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await client.GeoAddAsync(key, emptyEntries)); } @@ -111,7 +111,7 @@ public async Task GeoAdd_WrongKeyType_ThrowsException(BaseClient client) string key = Guid.NewGuid().ToString(); await client.StringSetAsync(key, "not_a_geo_key"); - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo")); } @@ -120,7 +120,7 @@ public async Task GeoAdd_WrongKeyType_ThrowsException(BaseClient client) public async Task GeoAdd_WithNX_OnlyAddsIfNotExists(BaseClient client) { string key = Guid.NewGuid().ToString(); - + // Add initial entries GeoEntry[] entries = [ @@ -129,7 +129,7 @@ public async Task GeoAdd_WithNX_OnlyAddsIfNotExists(BaseClient client) ]; long result = await client.GeoAddAsync(key, entries); Assert.Equal(2, result); - + // Try to add with NX option - should return 0 since members already exist GeoEntry[] newEntries = [ @@ -140,13 +140,13 @@ public async Task GeoAdd_WithNX_OnlyAddsIfNotExists(BaseClient client) long nxResult = await client.GeoAddAsync(key, newEntries, nxOptions); Assert.Equal(0, nxResult); } - + [Theory(DisableDiscoveryEnumeration = true)] [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task GeoAdd_WithXX_OnlyUpdatesIfExists(BaseClient client) { string key = Guid.NewGuid().ToString(); - + // Add initial entries GeoEntry[] entries = [ @@ -155,20 +155,20 @@ public async Task GeoAdd_WithXX_OnlyUpdatesIfExists(BaseClient client) ]; long result = await client.GeoAddAsync(key, entries); Assert.Equal(2, result); - + // Try to add new member with XX option - should return false since member doesn't exist var newEntry = new GeoEntry(32.0853, 34.7818, "Tel-Aviv"); var xxOptions = new GeoAddOptions(ConditionalChange.ONLY_IF_EXISTS); bool xxResult = await client.GeoAddAsync(key, newEntry, xxOptions); Assert.False(xxResult); } - + [Theory(DisableDiscoveryEnumeration = true)] [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task GeoAdd_WithCH_ReturnsChangedCount(BaseClient client) { string key = Guid.NewGuid().ToString(); - + // Add initial entries GeoEntry[] entries = [ @@ -177,7 +177,7 @@ public async Task GeoAdd_WithCH_ReturnsChangedCount(BaseClient client) ]; long result = await client.GeoAddAsync(key, entries); Assert.Equal(2, result); - + // Update existing member and add new member with CH option GeoEntry[] updateEntries = [ @@ -200,35 +200,29 @@ public async Task GeoDistance_AllUnits_ReturnsCorrectDistances(BaseClient client new GeoEntry(15.087269, 37.502669, "Catania") ]; await client.GeoAddAsync(key, entries); - + // Test all units with expected values (approximate distance between Palermo and Catania) double? distanceMeters = await client.GeoDistanceAsync(key, "Palermo", "Catania", GeoUnit.Meters); double? distanceKilometers = await client.GeoDistanceAsync(key, "Palermo", "Catania", GeoUnit.Kilometers); double? distanceMiles = await client.GeoDistanceAsync(key, "Palermo", "Catania", GeoUnit.Miles); double? distanceFeet = await client.GeoDistanceAsync(key, "Palermo", "Catania", GeoUnit.Feet); - - // Verify all distances are returned - Assert.NotNull(distanceMeters); - Assert.NotNull(distanceKilometers); - Assert.NotNull(distanceMiles); - Assert.NotNull(distanceFeet); - + // Verify approximate expected values (distance between Palermo and Catania) - Assert.True(distanceMeters > 166000 && distanceMeters < 167000); // ~166274 meters - Assert.True(distanceKilometers > 166 && distanceKilometers < 167); // ~166.27 km - Assert.True(distanceMiles > 103 && distanceMiles < 104); // ~103.31 miles - Assert.True(distanceFeet > 545000 && distanceFeet < 546000); // ~545,518 feet - + Assert.Equal(166274, distanceMeters.Value, 1000); // ~166274 meters + Assert.Equal(166.27, distanceKilometers.Value, 1); // ~166.27 km + Assert.Equal(103.31, distanceMiles.Value, 1); // ~103.31 miles + Assert.Equal(545518, distanceFeet.Value, 1000); // ~545,518 feet + // Verify unit conversions are consistent double metersToKm = distanceMeters.Value / 1000; double metersToMiles = distanceMeters.Value / 1609.344; double metersToFeet = distanceMeters.Value * 3.28084; - - Assert.True(Math.Abs(metersToKm - distanceKilometers.Value) < 0.001); - Assert.True(Math.Abs(metersToMiles - distanceMiles.Value) < 0.001); - Assert.True(Math.Abs(metersToFeet - distanceFeet.Value) < 1); // Allow 1 foot tolerance + + Assert.Equal(metersToKm, distanceKilometers.Value, 0.001); + Assert.Equal(metersToMiles, distanceMiles.Value, 0.001); + Assert.Equal(metersToFeet, distanceFeet.Value, 1); } - + [Theory(DisableDiscoveryEnumeration = true)] [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task GeoDistance_DefaultUnit_ReturnsMeters(BaseClient client) @@ -240,16 +234,16 @@ public async Task GeoDistance_DefaultUnit_ReturnsMeters(BaseClient client) new GeoEntry(15.087269, 37.502669, "Catania") ]; await client.GeoAddAsync(key, entries); - + // Test default unit (should be meters) double? distanceDefault = await client.GeoDistanceAsync(key, "Palermo", "Catania"); double? distanceMeters = await client.GeoDistanceAsync(key, "Palermo", "Catania", GeoUnit.Meters); - + Assert.NotNull(distanceDefault); Assert.NotNull(distanceMeters); Assert.Equal(distanceMeters.Value, distanceDefault.Value, 1e-9); } - + [Theory(DisableDiscoveryEnumeration = true)] [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task GeoSearch_AllUnits_ReturnsConsistentResults(BaseClient client) @@ -262,34 +256,40 @@ public async Task GeoSearch_AllUnits_ReturnsConsistentResults(BaseClient client) new GeoEntry(12.758489, 38.788135, "Trapani") ]; await client.GeoAddAsync(key, entries); - + var position = new GeoPosition(13.361389, 38.115556); // Palermo coordinates - + // Test search with different units - all should return same members but with different radius values var shapeMeters = new GeoSearchCircle(200000, GeoUnit.Meters); // 200km in meters var shapeKilometers = new GeoSearchCircle(200, GeoUnit.Kilometers); // 200km var shapeMiles = new GeoSearchCircle(124.27, GeoUnit.Miles); // ~200km in miles var shapeFeet = new GeoSearchCircle(656168, GeoUnit.Feet); // ~200km in feet - + GeoRadiusResult[] resultsMeters = await client.GeoSearchAsync(key, position, shapeMeters); GeoRadiusResult[] resultsKilometers = await client.GeoSearchAsync(key, position, shapeKilometers); GeoRadiusResult[] resultsMiles = await client.GeoSearchAsync(key, position, shapeMiles); GeoRadiusResult[] resultsFeet = await client.GeoSearchAsync(key, position, shapeFeet); - + // All searches should return the same members (Palermo and Catania should be within 200km) Assert.NotEmpty(resultsMeters); Assert.NotEmpty(resultsKilometers); Assert.NotEmpty(resultsMiles); Assert.NotEmpty(resultsFeet); - + // Should return same number of results Assert.Equal(resultsMeters.Length, resultsKilometers.Length); Assert.Equal(resultsMeters.Length, resultsMiles.Length); Assert.Equal(resultsMeters.Length, resultsFeet.Length); - - // Should contain Palermo and Catania + + // Should contain Palermo and Catania in all unit searches Assert.Contains("Palermo", resultsMeters.Select(r => r.Member.ToString())); Assert.Contains("Catania", resultsMeters.Select(r => r.Member.ToString())); + Assert.Contains("Palermo", resultsKilometers.Select(r => r.Member.ToString())); + Assert.Contains("Catania", resultsKilometers.Select(r => r.Member.ToString())); + Assert.Contains("Palermo", resultsMiles.Select(r => r.Member.ToString())); + Assert.Contains("Catania", resultsMiles.Select(r => r.Member.ToString())); + Assert.Contains("Palermo", resultsFeet.Select(r => r.Member.ToString())); + Assert.Contains("Catania", resultsFeet.Select(r => r.Member.ToString())); } [Theory(DisableDiscoveryEnumeration = true)] @@ -303,9 +303,8 @@ public async Task GeoHash_NonExistentMembers_ReturnsNulls(BaseClient client) new GeoEntry(15.087269, 37.502669, "Catania") ]; await client.GeoAddAsync(key, entries); - + string?[] hashes = await client.GeoHashAsync(key, new ValkeyValue[] { "Palermo", "Catania", "NonExistent" }); - Assert.Equal(3, hashes.Length); Assert.NotNull(hashes[0]); // Palermo Assert.NotNull(hashes[1]); // Catania Assert.Null(hashes[2]); // NonExistent @@ -317,9 +316,8 @@ public async Task GeoPosition_NonExistentMembers_ReturnsNulls(BaseClient client) { string key = Guid.NewGuid().ToString(); await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo"); - + GeoPosition?[] positions = await client.GeoPositionAsync(key, new ValkeyValue[] { "Palermo", "NonExistent" }); - Assert.Equal(2, positions.Length); Assert.NotNull(positions[0]); // Palermo Assert.Null(positions[1]); // NonExistent } @@ -330,7 +328,7 @@ public async Task GeoDistance_NonExistentMember_ReturnsNull(BaseClient client) { string key = Guid.NewGuid().ToString(); await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo"); - + double? distance = await client.GeoDistanceAsync(key, "Palermo", "NonExistent"); Assert.Null(distance); } @@ -340,19 +338,19 @@ public async Task GeoDistance_NonExistentMember_ReturnsNull(BaseClient client) public async Task Geo_NonExistentKey_ReturnsAppropriateDefaults(BaseClient client) { string key = Guid.NewGuid().ToString(); // Non-existent key - + // GeoDistance should return null for non-existent key double? distance = await client.GeoDistanceAsync(key, "member1", "member2"); Assert.Null(distance); - + // GeoHash should return null for non-existent key string? hash = await client.GeoHashAsync(key, "member"); Assert.Null(hash); - + // GeoPosition should return null for non-existent key GeoPosition? position = await client.GeoPositionAsync(key, "member"); Assert.Null(position); - + // GeoSearch should return empty array for non-existent key var searchPosition = new GeoPosition(13.361389, 38.115556); var shape = new GeoSearchCircle(100, GeoUnit.Kilometers); @@ -372,10 +370,10 @@ public async Task GeoDistance_ReturnsCorrectDistance(BaseClient client) ]; await client.GeoAddAsync(key, entries); - + double? distance = await client.GeoDistanceAsync(key, "Palermo", "Catania", GeoUnit.Kilometers); Assert.NotNull(distance); - Assert.True(distance > 160 && distance < 170); // Approximate distance between Palermo and Catania + Assert.Equal(166.27, distance.Value, 1); // Approximate distance between Palermo and Catania } [Theory(DisableDiscoveryEnumeration = true)] @@ -384,8 +382,8 @@ public async Task GeoDistance_WrongKeyType_ThrowsException(BaseClient client) { string key = Guid.NewGuid().ToString(); await client.StringSetAsync(key, "not_a_geo_key"); - - await Assert.ThrowsAsync(async () => + + await Assert.ThrowsAsync(async () => await client.GeoDistanceAsync(key, "member1", "member2")); } @@ -395,7 +393,7 @@ public async Task GeoHash_SingleMember_ReturnsHash(BaseClient client) { string key = Guid.NewGuid().ToString(); await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo"); - + string? hash = await client.GeoHashAsync(key, "Palermo"); Assert.NotNull(hash); Assert.NotEmpty(hash); @@ -412,9 +410,8 @@ public async Task GeoHash_MultipleMembers_ReturnsHashes(BaseClient client) new GeoEntry(15.087269, 37.502669, "Catania") ]; await client.GeoAddAsync(key, entries); - + string?[] hashes = await client.GeoHashAsync(key, new ValkeyValue[] { "Palermo", "Catania" }); - Assert.Equal(2, hashes.Length); Assert.NotNull(hashes[0]); Assert.NotNull(hashes[1]); Assert.NotEmpty(hashes[0]); @@ -427,7 +424,7 @@ public async Task GeoHash_NonExistentMember_ReturnsNull(BaseClient client) { string key = Guid.NewGuid().ToString(); await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo"); - + string? hash = await client.GeoHashAsync(key, "NonExistent"); Assert.Null(hash); } @@ -438,8 +435,8 @@ public async Task GeoHash_WrongKeyType_ThrowsException(BaseClient client) { string key = Guid.NewGuid().ToString(); await client.StringSetAsync(key, "not_a_geo_key"); - - await Assert.ThrowsAsync(async () => + + await Assert.ThrowsAsync(async () => await client.GeoHashAsync(key, "member")); } @@ -449,7 +446,7 @@ public async Task GeoHash_EmptyMembers_ReturnsEmptyArray(BaseClient client) { string key = Guid.NewGuid().ToString(); await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo"); - + string?[] hashes = await client.GeoHashAsync(key, new ValkeyValue[] { }); Assert.Empty(hashes); } @@ -462,7 +459,7 @@ public async Task GeoPosition_SingleMember_ReturnsPosition(BaseClient client) double longitude = 13.361389; double latitude = 38.115556; await client.GeoAddAsync(key, longitude, latitude, "Palermo"); - + GeoPosition? position = await client.GeoPositionAsync(key, "Palermo"); Assert.NotNull(position); Assert.True(Math.Abs(position.Value.Longitude - longitude) < 0.001); @@ -480,9 +477,8 @@ public async Task GeoPosition_MultipleMembers_ReturnsPositions(BaseClient client new GeoEntry(15.087269, 37.502669, "Catania") ]; await client.GeoAddAsync(key, entries); - + GeoPosition?[] positions = await client.GeoPositionAsync(key, new ValkeyValue[] { "Palermo", "Catania" }); - Assert.Equal(2, positions.Length); Assert.NotNull(positions[0]); Assert.NotNull(positions[1]); Assert.True(Math.Abs(positions[0]!.Value.Longitude - 13.361389) < 0.001); @@ -497,7 +493,7 @@ public async Task GeoPosition_NonExistentMember_ReturnsNull(BaseClient client) { string key = Guid.NewGuid().ToString(); await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo"); - + GeoPosition? position = await client.GeoPositionAsync(key, "NonExistent"); Assert.Null(position); } @@ -508,8 +504,8 @@ public async Task GeoPosition_WrongKeyType_ThrowsException(BaseClient client) { string key = Guid.NewGuid().ToString(); await client.StringSetAsync(key, "not_a_geo_key"); - - await Assert.ThrowsAsync(async () => + + await Assert.ThrowsAsync(async () => await client.GeoPositionAsync(key, "member")); } @@ -525,10 +521,10 @@ public async Task GeoSearch_FromMember_ByRadius_ReturnsMembers(BaseClient client new GeoEntry(12.758489, 38.788135, "edge") ]; await client.GeoAddAsync(key, entries); - + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); GeoRadiusResult[] results = await client.GeoSearchAsync(key, "Palermo", shape); - + Assert.NotEmpty(results); Assert.Contains("Palermo", results.Select(r => r.Member.ToString())); Assert.Contains("Catania", results.Select(r => r.Member.ToString())); @@ -545,11 +541,11 @@ public async Task GeoSearch_FromPosition_ByRadius_ReturnsMembers(BaseClient clie new GeoEntry(15.087269, 37.502669, "Catania") ]; await client.GeoAddAsync(key, entries); - + var position = new GeoPosition(13.361389, 38.115556); // Palermo coordinates var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); GeoRadiusResult[] results = await client.GeoSearchAsync(key, position, shape); - + Assert.NotEmpty(results); Assert.Contains("Palermo", results.Select(r => r.Member.ToString())); Assert.Contains("Catania", results.Select(r => r.Member.ToString())); @@ -567,10 +563,10 @@ public async Task GeoSearch_FromMember_ByBox_ReturnsMembers(BaseClient client) new GeoEntry(12.758489, 38.788135, "edge") ]; await client.GeoAddAsync(key, entries); - + var shape = new GeoSearchBox(400, 400, GeoUnit.Kilometers); GeoRadiusResult[] results = await client.GeoSearchAsync(key, "Palermo", shape); - + Assert.NotEmpty(results); Assert.Contains("Palermo", results.Select(r => r.Member.ToString())); Assert.Contains("Catania", results.Select(r => r.Member.ToString())); @@ -587,11 +583,11 @@ public async Task GeoSearch_FromPosition_ByBox_ReturnsMembers(BaseClient client) new GeoEntry(15.087269, 37.502669, "Catania") ]; await client.GeoAddAsync(key, entries); - + var position = new GeoPosition(13.361389, 38.115556); // Palermo coordinates var shape = new GeoSearchBox(400, 400, GeoUnit.Kilometers); GeoRadiusResult[] results = await client.GeoSearchAsync(key, position, shape); - + Assert.NotEmpty(results); Assert.Contains("Palermo", results.Select(r => r.Member.ToString())); Assert.Contains("Catania", results.Select(r => r.Member.ToString())); @@ -612,11 +608,11 @@ public async Task GeoSearch_WithCount_LimitsResults(BaseClient client) new GeoEntry(14.015482, 37.734741, "edge2") ]; await client.GeoAddAsync(key, entries); - + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); GeoRadiusResult[] allResults = await client.GeoSearchAsync(key, "Palermo", shape); GeoRadiusResult[] limitedResults = await client.GeoSearchAsync(key, "Palermo", shape, 2); - + Assert.True(allResults.Length >= 2); Assert.Equal(2, limitedResults.Length); } @@ -635,20 +631,20 @@ public async Task GeoSearch_WithDemandClosest_VerifiesParameterUsage(BaseClient new GeoEntry(13.5, 38.0, "close1") ]; await client.GeoAddAsync(key, entries); - + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); - + // Test that demandClosest=true works (should return closest results) GeoRadiusResult[] closestResults = await client.GeoSearchAsync(key, "Palermo", shape, 3, true); Assert.Equal(3, closestResults.Length); Assert.Contains("Palermo", closestResults.Select(r => r.Member.ToString())); Assert.Contains("close1", closestResults.Select(r => r.Member.ToString())); // close1 should be in closest results - + // Test that demandClosest=false works (should return any results, not necessarily closest) GeoRadiusResult[] anyResults = await client.GeoSearchAsync(key, "Palermo", shape, 3, false); Assert.Equal(3, anyResults.Length); Assert.Contains("Palermo", anyResults.Select(r => r.Member.ToString())); - + // Both should return valid results, verifying the parameter is accepted Assert.NotEmpty(closestResults); Assert.NotEmpty(anyResults); @@ -666,17 +662,17 @@ public async Task GeoSearch_WithOrder_ReturnsOrderedResults(BaseClient client) new GeoEntry(12.758489, 38.788135, "edge1") ]; await client.GeoAddAsync(key, entries); - + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); - + // Test ascending order GeoRadiusResult[] ascResults = await client.GeoSearchAsync(key, "Palermo", shape, order: Order.Ascending); Assert.NotEmpty(ascResults); - + // Test descending order GeoRadiusResult[] descResults = await client.GeoSearchAsync(key, "Palermo", shape, order: Order.Descending); Assert.NotEmpty(descResults); - + // Verify both return same count but potentially different order Assert.Equal(ascResults.Length, descResults.Length); } @@ -692,24 +688,24 @@ public async Task GeoSearch_WithOptions_ReturnsEnrichedResults(BaseClient client new GeoEntry(15.087269, 37.502669, "Catania") ]; await client.GeoAddAsync(key, entries); - + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); - + // Test with distance option GeoRadiusResult[] distResults = await client.GeoSearchAsync(key, "Palermo", shape, options: GeoRadiusOptions.WithDistance); Assert.NotEmpty(distResults); - + var palermoDist = distResults.FirstOrDefault(r => r.Member.ToString() == "Palermo"); Assert.NotNull(palermoDist); Assert.Equal("Palermo", palermoDist.Member.ToString()); Assert.NotNull(palermoDist.Distance); // Should have distance Assert.Equal(0.0, palermoDist.Distance.Value, 1); // Distance from itself should be ~0 Assert.Null(palermoDist.Position); // Should be null without WithCoordinates - + // Test with coordinates option GeoRadiusResult[] coordResults = await client.GeoSearchAsync(key, "Palermo", shape, options: GeoRadiusOptions.WithCoordinates); Assert.NotEmpty(coordResults); - + var palermoCoord = coordResults.FirstOrDefault(r => r.Member.ToString() == "Palermo"); Assert.NotNull(palermoCoord); Assert.Equal("Palermo", palermoCoord.Member.ToString()); @@ -728,20 +724,20 @@ public async Task GeoSearch_WithDistance_ReturnsAccurateDistances(BaseClient cli new GeoEntry(15.087269, 37.502669, "Catania") ]; await client.GeoAddAsync(key, entries); - + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); GeoRadiusResult[] results = await client.GeoSearchAsync(key, "Palermo", shape, options: GeoRadiusOptions.WithDistance); - + var palermoResult = results.FirstOrDefault(r => r.Member.ToString() == "Palermo"); var cataniaResult = results.FirstOrDefault(r => r.Member.ToString() == "Catania"); - + Assert.NotNull(palermoResult); Assert.NotNull(cataniaResult); Assert.NotNull(palermoResult.Distance); Assert.NotNull(cataniaResult.Distance); - + Assert.Equal(0.0, palermoResult.Distance.Value, 1); // Distance from itself should be ~0 - Assert.True(cataniaResult.Distance.Value > 160 && cataniaResult.Distance.Value < 170); // ~166km between cities + Assert.Equal(166.27, cataniaResult.Distance.Value, 1); // ~166km between cities } [Theory(DisableDiscoveryEnumeration = true)] @@ -751,7 +747,7 @@ public async Task GeoSearchAndStore_WithMember_StoresResults(BaseClient client) string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; string sourceKey = keyPrefix + ":source"; string destinationKey = keyPrefix + ":dest"; - + GeoEntry[] entries = [ new GeoEntry(13.361389, 38.115556, "Palermo"), @@ -759,12 +755,12 @@ public async Task GeoSearchAndStore_WithMember_StoresResults(BaseClient client) new GeoEntry(12.758489, 38.788135, "Trapani") ]; await client.GeoAddAsync(sourceKey, entries); - + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); long count = await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, "Palermo", shape); - + Assert.Equal(3, count); - + ValkeyValue[] storedMembers = await client.SortedSetRangeByRankAsync(destinationKey, 0, -1); Assert.Equal(3, storedMembers.Length); Assert.Contains("Palermo", storedMembers.Select(r => r.ToString())); @@ -779,18 +775,18 @@ public async Task GeoSearchAndStore_WithPosition_StoresResults(BaseClient client string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; string sourceKey = keyPrefix + ":source"; string destinationKey = keyPrefix + ":dest"; - + GeoEntry[] entries = [ new GeoEntry(13.361389, 38.115556, "Palermo"), new GeoEntry(15.087269, 37.502669, "Catania") ]; await client.GeoAddAsync(sourceKey, entries); - + var position = new GeoPosition(13.361389, 38.115556); var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); long count = await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, position, shape); - + Assert.True(count >= 1); Assert.Contains("Palermo", (await client.SortedSetRangeByRankAsync(destinationKey, 0, -1)).Select(r => r.ToString())); } @@ -802,25 +798,25 @@ public async Task GeoSearchAndStore_WithDistances_StoresDistances(BaseClient cli string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; string sourceKey = keyPrefix + ":source"; string destinationKey = keyPrefix + ":dest"; - + GeoEntry[] entries = [ new GeoEntry(13.361389, 38.115556, "Palermo"), new GeoEntry(15.087269, 37.502669, "Catania") ]; await client.GeoAddAsync(sourceKey, entries); - + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); long count = await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, "Palermo", shape, storeDistances: true); - + Assert.Equal(2, count); - + var results = await client.SortedSetRangeByRankWithScoresAsync(destinationKey, 0, -1); Assert.Equal(2, results.Length); - + var palermoResult = results.FirstOrDefault(r => r.Key.ToString() == "Palermo"); var cataniaResult = results.FirstOrDefault(r => r.Key.ToString() == "Catania"); - + Assert.NotNull(palermoResult); Assert.NotNull(cataniaResult); Assert.Equal(0.0, palermoResult.Value, 0.1); @@ -833,9 +829,9 @@ public async Task GeoSearch_NonExistentMember_ThrowsException(BaseClient client) { string key = Guid.NewGuid().ToString(); await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo"); - + var shape = new GeoSearchCircle(100, GeoUnit.Kilometers); - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await client.GeoSearchAsync(key, "NonExistentMember", shape)); } @@ -845,10 +841,10 @@ public async Task GeoSearch_WrongKeyType_ThrowsException(BaseClient client) { string key = Guid.NewGuid().ToString(); await client.StringSetAsync(key, "not_a_geo_key"); - + var position = new GeoPosition(13.361389, 38.115556); var shape = new GeoSearchCircle(100, GeoUnit.Kilometers); - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await client.GeoSearchAsync(key, position, shape)); } @@ -858,11 +854,11 @@ public async Task GeoSearch_NoMembersInArea_ReturnsEmpty(BaseClient client) { string key = Guid.NewGuid().ToString(); await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo"); - + var position = new GeoPosition(0.0, 0.0); // Far from Palermo var shape = new GeoSearchCircle(1, GeoUnit.Meters); // Very small radius GeoRadiusResult[] results = await client.GeoSearchAsync(key, position, shape); - + Assert.Empty(results); } @@ -873,11 +869,11 @@ public async Task GeoSearchAndStore_NonExistentMember_ThrowsException(BaseClient string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; string sourceKey = keyPrefix + ":source"; string destinationKey = keyPrefix + ":dest"; - + await client.GeoAddAsync(sourceKey, 13.361389, 38.115556, "Palermo"); - + var shape = new GeoSearchCircle(100, GeoUnit.Kilometers); - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, "NonExistentMember", shape)); } @@ -888,12 +884,12 @@ public async Task GeoSearchAndStore_WrongKeyType_ThrowsException(BaseClient clie string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; string sourceKey = keyPrefix + ":source"; string destinationKey = keyPrefix + ":dest"; - + await client.StringSetAsync(sourceKey, "not_a_geo_key"); - + var position = new GeoPosition(13.361389, 38.115556); var shape = new GeoSearchCircle(100, GeoUnit.Kilometers); - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, position, shape)); } @@ -904,7 +900,7 @@ public async Task GeoSearchAndStore_WithCount_LimitsStoredResults(BaseClient cli string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; string sourceKey = keyPrefix + ":source"; string destinationKey = keyPrefix + ":dest"; - + GeoEntry[] entries = [ new GeoEntry(13.361389, 38.115556, "Palermo"), @@ -913,12 +909,12 @@ public async Task GeoSearchAndStore_WithCount_LimitsStoredResults(BaseClient cli new GeoEntry(14.015482, 37.734741, "Enna") ]; await client.GeoAddAsync(sourceKey, entries); - + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); long count = await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, "Palermo", shape, count: 2); - + Assert.Equal(2, count); - + ValkeyValue[] storedMembers = await client.SortedSetRangeByRankAsync(destinationKey, 0, -1); Assert.Equal(2, storedMembers.Length); } @@ -930,22 +926,22 @@ public async Task GeoSearchAndStore_OverwritesDestination(BaseClient client) string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; string sourceKey = keyPrefix + ":source"; string destinationKey = keyPrefix + ":dest"; - + GeoEntry[] entries = [ new GeoEntry(13.361389, 38.115556, "Palermo"), new GeoEntry(15.087269, 37.502669, "Catania") ]; await client.GeoAddAsync(sourceKey, entries); - + await client.SortedSetAddAsync(destinationKey, new SortedSetEntry[] { new SortedSetEntry("OldMember", 100) }); Assert.Equal(1, await client.SortedSetCardAsync(destinationKey)); - + var shape = new GeoSearchCircle(200, GeoUnit.Kilometers); long count = await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, "Palermo", shape); - + Assert.Equal(2, count); - + ValkeyValue[] storedMembers = await client.SortedSetRangeByRankAsync(destinationKey, 0, -1); Assert.Equal(2, storedMembers.Length); Assert.DoesNotContain("OldMember", storedMembers.Select(m => m.ToString())); @@ -960,15 +956,15 @@ public async Task GeoSearchAndStore_NoResults_CreatesEmptyDestination(BaseClient string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; string sourceKey = keyPrefix + ":source"; string destinationKey = keyPrefix + ":dest"; - + await client.GeoAddAsync(sourceKey, 13.361389, 38.115556, "Palermo"); - + var position = new GeoPosition(0.0, 0.0); var shape = new GeoSearchCircle(1, GeoUnit.Meters); long count = await client.GeoSearchAndStoreAsync(sourceKey, destinationKey, position, shape); - + Assert.Equal(0, count); Assert.Equal(0, await client.SortedSetCardAsync(destinationKey)); // Verify destination key exists but is empty - TypeAsync not available in BaseClient } -} \ No newline at end of file +} From e05fd117831f2a51d9a4b308bbf7b2e11a6c1875 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Fri, 17 Oct 2025 13:05:21 -0700 Subject: [PATCH 19/27] fix test Signed-off-by: Alex Rehnby-Martin --- .../GeospatialCommandTests.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs index af55b6d4..acab1b90 100644 --- a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs @@ -208,19 +208,19 @@ public async Task GeoDistance_AllUnits_ReturnsCorrectDistances(BaseClient client double? distanceFeet = await client.GeoDistanceAsync(key, "Palermo", "Catania", GeoUnit.Feet); // Verify approximate expected values (distance between Palermo and Catania) - Assert.Equal(166274, distanceMeters.Value, 1000); // ~166274 meters - Assert.Equal(166.27, distanceKilometers.Value, 1); // ~166.27 km - Assert.Equal(103.31, distanceMiles.Value, 1); // ~103.31 miles - Assert.Equal(545518, distanceFeet.Value, 1000); // ~545,518 feet + Assert.True(Math.Abs(distanceMeters.Value - 166274) < 1000); // ~166274 meters + Assert.True(Math.Abs(distanceKilometers.Value - 166.27) < 1); // ~166.27 km + Assert.True(Math.Abs(distanceMiles.Value - 103.31) < 1); // ~103.31 miles + Assert.True(Math.Abs(distanceFeet.Value - 545518) < 1000); // ~545,518 feet // Verify unit conversions are consistent double metersToKm = distanceMeters.Value / 1000; double metersToMiles = distanceMeters.Value / 1609.344; double metersToFeet = distanceMeters.Value * 3.28084; - Assert.Equal(metersToKm, distanceKilometers.Value, 0.001); - Assert.Equal(metersToMiles, distanceMiles.Value, 0.001); - Assert.Equal(metersToFeet, distanceFeet.Value, 1); + Assert.True(Math.Abs(metersToKm - distanceKilometers.Value) < 0.01); + Assert.True(Math.Abs(metersToMiles - distanceMiles.Value) < 0.01); + Assert.True(Math.Abs(metersToFeet - distanceFeet.Value) < 10); } [Theory(DisableDiscoveryEnumeration = true)] @@ -239,7 +239,7 @@ public async Task GeoDistance_DefaultUnit_ReturnsMeters(BaseClient client) double? distanceDefault = await client.GeoDistanceAsync(key, "Palermo", "Catania"); double? distanceMeters = await client.GeoDistanceAsync(key, "Palermo", "Catania", GeoUnit.Meters); - Assert.Equal(distanceMeters.Value, distanceDefault.Value, 1e-9); + Assert.True(Math.Abs(distanceMeters.Value - distanceDefault.Value) < 1e-9); } [Theory(DisableDiscoveryEnumeration = true)] From 204928d42ff493a9ef80f49325d55e9c7d59f3c4 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Fri, 17 Oct 2025 13:16:54 -0700 Subject: [PATCH 20/27] Format Signed-off-by: Alex Rehnby-Martin --- sources/Valkey.Glide/ConditionalChange.cs | 2 +- sources/Valkey.Glide/GeoAddOptions.cs | 2 +- sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs | 4 ++-- sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs | 2 +- sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sources/Valkey.Glide/ConditionalChange.cs b/sources/Valkey.Glide/ConditionalChange.cs index 9b79df06..f119f55e 100644 --- a/sources/Valkey.Glide/ConditionalChange.cs +++ b/sources/Valkey.Glide/ConditionalChange.cs @@ -16,4 +16,4 @@ public enum ConditionalChange /// Only update elements that already exist. Don't add new elements. /// ONLY_IF_EXISTS -} \ No newline at end of file +} diff --git a/sources/Valkey.Glide/GeoAddOptions.cs b/sources/Valkey.Glide/GeoAddOptions.cs index adaad028..556ccd13 100644 --- a/sources/Valkey.Glide/GeoAddOptions.cs +++ b/sources/Valkey.Glide/GeoAddOptions.cs @@ -47,4 +47,4 @@ public GeoAddOptions(ConditionalChange conditionalChange, bool changed) ConditionalChange = conditionalChange; Changed = changed; } -} \ No newline at end of file +} diff --git a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs index ca06ec5c..5c398b3c 100644 --- a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs @@ -123,7 +123,7 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoA public static Cmd GeoPositionAsync(ValkeyKey key, ValkeyValue member) { GlideString[] args = [key.ToGlideString(), member.ToGlideString()]; - return new(RequestType.GeoPos, args, false, response => + return new(RequestType.GeoPos, args, false, response => response.Length > 0 ? ParseGeoPosition(response[0]) : null); } @@ -136,7 +136,7 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoA public static Cmd GeoPositionAsync(ValkeyKey key, ValkeyValue[] members) { GlideString[] args = [key.ToGlideString(), .. members.Select(m => m.ToGlideString())]; - return new(RequestType.GeoPos, args, false, response => + return new(RequestType.GeoPos, args, false, response => response.Select(ParseGeoPosition).ToArray()); } diff --git a/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs b/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs index 576ee836..6e593d43 100644 --- a/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs +++ b/sources/Valkey.Glide/Pipeline/BaseBatch.GeospatialCommands.cs @@ -61,4 +61,4 @@ public abstract partial class BaseBatch IBatch IBatchGeospatialCommands.GeoSearch(ValkeyKey key, ValkeyValue fromMember, GeoSearchShape shape, long count, bool demandClosest, Order? order, GeoRadiusOptions options) => GeoSearch(key, fromMember, shape, count, demandClosest, order, options); IBatch IBatchGeospatialCommands.GeoSearch(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape, long count, bool demandClosest, Order? order, GeoRadiusOptions options) => GeoSearch(key, fromPosition, shape, count, demandClosest, order, options); -} \ No newline at end of file +} diff --git a/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs b/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs index ad058a0a..72d768e6 100644 --- a/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs +++ b/sources/Valkey.Glide/Pipeline/IBatchGeospatialCommands.cs @@ -55,4 +55,4 @@ internal interface IBatchGeospatialCommands IBatch GeoSearch(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default); -} \ No newline at end of file +} From 9eda37119084b4efdc373c73e9eee04d969873f6 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Fri, 17 Oct 2025 16:18:44 -0700 Subject: [PATCH 21/27] Fix Signed-off-by: Alex Rehnby-Martin --- tests/Valkey.Glide.UnitTests/CommandTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Valkey.Glide.UnitTests/CommandTests.cs b/tests/Valkey.Glide.UnitTests/CommandTests.cs index 3a9ffd7b..a20e5cb8 100644 --- a/tests/Valkey.Glide.UnitTests/CommandTests.cs +++ b/tests/Valkey.Glide.UnitTests/CommandTests.cs @@ -302,7 +302,7 @@ public void ValidateCommandArgs() () => Assert.Equal(["GEOSEARCH", "key", "FROMLONLAT", "13.361389000000001", "38.115555999999998", "BYRADIUS", "200", "m"], Request.GeoSearchAsync("key", new GeoPosition(13.361389, 38.115556), new GeoSearchCircle(200, GeoUnit.Meters)).GetArgs()), () => Assert.Equal(["GEOSEARCH", "key", "FROMMEMBER", "Palermo", "BYBOX", "300", "400", "km"], Request.GeoSearchAsync("key", "Palermo", new GeoSearchBox(400, 300, GeoUnit.Kilometers)).GetArgs()), () => Assert.Equal(["GEOSEARCHSTORE", "dest", "key", "FROMMEMBER", "Palermo", "BYRADIUS", "100", "km"], Request.GeoSearchAndStoreAsync("key", "dest", "Palermo", new GeoSearchCircle(100, GeoUnit.Kilometers), -1, true, null, false).GetArgs()), - () => Assert.Equal(["GEOSEARCHSTORE", "dest", "key", "FROMLONLAT", "13.361389000000001", "38.115555999999998", "BYRADIUS", "200", "m", "STOREDIST"], Request.GeoSearchAndStoreAsync("key", "dest", new GeoPosition(13.361389, 38.115556), new GeoSearchCircle(200, GeoUnit.Meters), -1, true, null, true).GetArgs()) + () => Assert.Equal(["GEOSEARCHSTORE", "dest", "key", "FROMLONLAT", "13.361389000000001", "38.115555999999998", "BYRADIUS", "200", "m", "STOREDIST"], Request.GeoSearchAndStoreAsync("key", "dest", new GeoPosition(13.361389, 38.115556), new GeoSearchCircle(200, GeoUnit.Meters), -1, true, null, true).GetArgs()), // HyperLogLog Commands () => Assert.Equal(["PFADD", "key", "element"], Request.HyperLogLogAddAsync("key", "element").GetArgs()), () => Assert.Equal(["PFADD", "key", "element1", "element2", "element3"], Request.HyperLogLogAddAsync("key", ["element1", "element2", "element3"]).GetArgs()), From 94b3047fef9b009a4a1c3fc646d1a8a7a0cd7614 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Sun, 19 Oct 2025 21:07:38 -0700 Subject: [PATCH 22/27] Format Signed-off-by: Alex Rehnby-Martin --- .../Internals/Request.GeospatialCommands.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs index 5c398b3c..27a5610b 100644 --- a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs @@ -154,7 +154,7 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoA public static Cmd GeoSearchAsync(ValkeyKey key, ValkeyValue fromMember, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.None) { List args = [key.ToGlideString(), ValkeyLiterals.FROMMEMBER.ToGlideString(), fromMember.ToGlideString()]; - List shapeArgs = []; + var shapeArgs = new List(); shape.AddArgs(shapeArgs); args.AddRange(shapeArgs.Select(a => a.ToGlideString())); if (count > 0) @@ -190,7 +190,7 @@ public static Cmd GeoSearchAsync(ValkeyKey key, Val public static Cmd GeoSearchAsync(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.None) { List args = [key.ToGlideString(), ValkeyLiterals.FROMLONLAT.ToGlideString(), fromPosition.Longitude.ToGlideString(), fromPosition.Latitude.ToGlideString()]; - List shapeArgs = []; + var shapeArgs = new List(); shape.AddArgs(shapeArgs); args.AddRange(shapeArgs.Select(a => a.ToGlideString())); if (count > 0) @@ -229,17 +229,17 @@ private static GeoRadiusResult[] ProcessGeoSearchResponse(object[] response, Geo // If no options are specified, Redis returns simple strings (member names) if (options == GeoRadiusOptions.None) { - return new GeoRadiusResult(new ValkeyValue(item?.ToString()), null, null, null); + return new GeoRadiusResult(new ValkeyValue(item?.ToString() ?? ""), null, null, null); } // With options, Redis returns arrays: [member, ...additional data based on options] if (item is not object[] itemArray || itemArray.Length == 0) { // Fallback for unexpected format - return new GeoRadiusResult(new ValkeyValue(item?.ToString()), null, null, null); + return new GeoRadiusResult(new ValkeyValue(item?.ToString() ?? ""), null, null, null); } - var member = new ValkeyValue(itemArray[0]?.ToString()); + var member = new ValkeyValue(itemArray[0]?.ToString() ?? ""); double? distance = null; long? hash = null; GeoPosition? position = null; From 756334ef8d2c2f7aef30dda4135cad783e95396f Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Sun, 19 Oct 2025 21:29:03 -0700 Subject: [PATCH 23/27] Format Signed-off-by: Alex Rehnby-Martin --- .../Internals/Request.GeospatialCommands.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs index 27a5610b..02fe4581 100644 --- a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs @@ -206,10 +206,10 @@ public static Cmd GeoSearchAsync(ValkeyKey key, Geo { args.Add(order.Value.ToLiteral().ToGlideString()); } - List optionArgs = []; + var optionArgs = new List(); options.AddArgs(optionArgs); args.AddRange(optionArgs.Select(a => a.ToGlideString())); - return new(RequestType.GeoSearch, [.. args], false, response => ProcessGeoSearchResponse(response, options)); + return new(RequestType.GeoSearch, args.ToArray(), false, response => ProcessGeoSearchResponse(response, options)); } @@ -302,7 +302,7 @@ private static GeoRadiusResult[] ProcessGeoSearchResponse(object[] response, Geo /// When true, stores distances instead of just member names. private static void AddGeoSearchAndStoreArgs(List args, GeoSearchShape shape, long count, bool demandClosest, Order? order, bool storeDistances) { - List shapeArgs = []; + var shapeArgs = new List(); shape.AddArgs(shapeArgs); args.AddRange(shapeArgs.Select(a => a.ToGlideString())); if (count > 0) @@ -340,7 +340,7 @@ public static Cmd GeoSearchAndStoreAsync(ValkeyKey sourceKey, Valkey { List args = [destinationKey.ToGlideString(), sourceKey.ToGlideString(), ValkeyLiterals.FROMMEMBER.ToGlideString(), fromMember.ToGlideString()]; AddGeoSearchAndStoreArgs(args, shape, count, demandClosest, order, storeDistances); - return Simple(RequestType.GeoSearchStore, [.. args]); + return Simple(RequestType.GeoSearchStore, args.ToArray()); } /// @@ -359,6 +359,6 @@ public static Cmd GeoSearchAndStoreAsync(ValkeyKey sourceKey, Valkey { List args = [destinationKey.ToGlideString(), sourceKey.ToGlideString(), ValkeyLiterals.FROMLONLAT.ToGlideString(), fromPosition.Longitude.ToGlideString(), fromPosition.Latitude.ToGlideString()]; AddGeoSearchAndStoreArgs(args, shape, count, demandClosest, order, storeDistances); - return Simple(RequestType.GeoSearchStore, [.. args]); + return Simple(RequestType.GeoSearchStore, args.ToArray()); } } From cd4a43d5cc69ddca0a506f9a40237f8595b8f6b9 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Mon, 20 Oct 2025 08:57:00 -0700 Subject: [PATCH 24/27] Format Signed-off-by: Alex Rehnby-Martin --- .../Internals/Request.GeospatialCommands.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs index 02fe4581..937e37a4 100644 --- a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs @@ -40,7 +40,7 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry value, GeoAddO args.Add(value.Longitude.ToGlideString()); args.Add(value.Latitude.ToGlideString()); args.Add(value.Member.ToGlideString()); - return Boolean(RequestType.GeoAdd, [.. args]); + return Boolean(RequestType.GeoAdd, args.ToArray()); } /// @@ -60,7 +60,7 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoA args.Add(value.Latitude.ToGlideString()); args.Add(value.Member.ToGlideString()); } - return Simple(RequestType.GeoAdd, [.. args]); + return Simple(RequestType.GeoAdd, args.ToArray()); } /// @@ -97,7 +97,7 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoA /// A with the request. public static Cmd GeoHashAsync(ValkeyKey key, ValkeyValue[] members) { - GlideString[] args = [key.ToGlideString(), .. members.Select(m => m.ToGlideString())]; + GlideString[] args = [key.ToGlideString(), ..members.Select(m => m.ToGlideString())]; return new(RequestType.GeoHash, args, false, response => response.Select(item => item?.ToString()).ToArray()); } @@ -135,7 +135,7 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoA /// A with the request. public static Cmd GeoPositionAsync(ValkeyKey key, ValkeyValue[] members) { - GlideString[] args = [key.ToGlideString(), .. members.Select(m => m.ToGlideString())]; + GlideString[] args = [key.ToGlideString(), ..members.Select(m => m.ToGlideString())]; return new(RequestType.GeoPos, args, false, response => response.Select(ParseGeoPosition).ToArray()); } @@ -173,7 +173,7 @@ public static Cmd GeoSearchAsync(ValkeyKey key, Val List optionArgs = []; options.AddArgs(optionArgs); args.AddRange(optionArgs.Select(a => a.ToGlideString())); - return new(RequestType.GeoSearch, [.. args], false, response => ProcessGeoSearchResponse(response, options)); + return new(RequestType.GeoSearch, args.ToArray(), false, response => ProcessGeoSearchResponse(response, options)); } /// From d5527623b1e2829c16e0f432cf68d83b3cd3a9dd Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Mon, 20 Oct 2025 09:13:13 -0700 Subject: [PATCH 25/27] Format Signed-off-by: Alex Rehnby-Martin --- sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs index 937e37a4..db8d3a4c 100644 --- a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs @@ -97,7 +97,7 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoA /// A with the request. public static Cmd GeoHashAsync(ValkeyKey key, ValkeyValue[] members) { - GlideString[] args = [key.ToGlideString(), ..members.Select(m => m.ToGlideString())]; + GlideString[] args = [key.ToGlideString(), .. members.Select(m => m.ToGlideString())]; return new(RequestType.GeoHash, args, false, response => response.Select(item => item?.ToString()).ToArray()); } @@ -135,7 +135,7 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoA /// A with the request. public static Cmd GeoPositionAsync(ValkeyKey key, ValkeyValue[] members) { - GlideString[] args = [key.ToGlideString(), ..members.Select(m => m.ToGlideString())]; + GlideString[] args = [key.ToGlideString(), .. members.Select(m => m.ToGlideString())]; return new(RequestType.GeoPos, args, false, response => response.Select(ParseGeoPosition).ToArray()); } From f3a52160b59c2fabe39f9f791bcc08bebc3cda2e Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Mon, 20 Oct 2025 09:39:15 -0700 Subject: [PATCH 26/27] Format Signed-off-by: Alex Rehnby-Martin --- .../Internals/Request.GeospatialCommands.cs | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs index db8d3a4c..e02016b8 100644 --- a/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.GeospatialCommands.cs @@ -40,7 +40,7 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry value, GeoAddO args.Add(value.Longitude.ToGlideString()); args.Add(value.Latitude.ToGlideString()); args.Add(value.Member.ToGlideString()); - return Boolean(RequestType.GeoAdd, args.ToArray()); + return Boolean(RequestType.GeoAdd, [.. args]); } /// @@ -60,7 +60,7 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoA args.Add(value.Latitude.ToGlideString()); args.Add(value.Member.ToGlideString()); } - return Simple(RequestType.GeoAdd, args.ToArray()); + return Simple(RequestType.GeoAdd, [.. args]); } /// @@ -97,8 +97,9 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoA /// A with the request. public static Cmd GeoHashAsync(ValkeyKey key, ValkeyValue[] members) { - GlideString[] args = [key.ToGlideString(), .. members.Select(m => m.ToGlideString())]; - return new(RequestType.GeoHash, args, false, response => response.Select(item => item?.ToString()).ToArray()); + var args = new List { key.ToGlideString() }; + args.AddRange(members.Select(m => m.ToGlideString())); + return new(RequestType.GeoHash, [.. args], false, response => [.. response.Select(item => item?.ToString())]); } /// @@ -135,9 +136,10 @@ public static Cmd GeoAddAsync(ValkeyKey key, GeoEntry[] values, GeoA /// A with the request. public static Cmd GeoPositionAsync(ValkeyKey key, ValkeyValue[] members) { - GlideString[] args = [key.ToGlideString(), .. members.Select(m => m.ToGlideString())]; - return new(RequestType.GeoPos, args, false, response => - response.Select(ParseGeoPosition).ToArray()); + var args = new List { key.ToGlideString() }; + args.AddRange(members.Select(m => m.ToGlideString())); + return new(RequestType.GeoPos, [.. args], false, response => + [.. response.Select(ParseGeoPosition)]); } /// @@ -173,7 +175,7 @@ public static Cmd GeoSearchAsync(ValkeyKey key, Val List optionArgs = []; options.AddArgs(optionArgs); args.AddRange(optionArgs.Select(a => a.ToGlideString())); - return new(RequestType.GeoSearch, args.ToArray(), false, response => ProcessGeoSearchResponse(response, options)); + return new(RequestType.GeoSearch, [.. args], false, response => ProcessGeoSearchResponse(response, options)); } /// @@ -190,7 +192,7 @@ public static Cmd GeoSearchAsync(ValkeyKey key, Val public static Cmd GeoSearchAsync(ValkeyKey key, GeoPosition fromPosition, GeoSearchShape shape, long count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.None) { List args = [key.ToGlideString(), ValkeyLiterals.FROMLONLAT.ToGlideString(), fromPosition.Longitude.ToGlideString(), fromPosition.Latitude.ToGlideString()]; - var shapeArgs = new List(); + List shapeArgs = []; shape.AddArgs(shapeArgs); args.AddRange(shapeArgs.Select(a => a.ToGlideString())); if (count > 0) @@ -206,10 +208,10 @@ public static Cmd GeoSearchAsync(ValkeyKey key, Geo { args.Add(order.Value.ToLiteral().ToGlideString()); } - var optionArgs = new List(); + List optionArgs = []; options.AddArgs(optionArgs); args.AddRange(optionArgs.Select(a => a.ToGlideString())); - return new(RequestType.GeoSearch, args.ToArray(), false, response => ProcessGeoSearchResponse(response, options)); + return new(RequestType.GeoSearch, [.. args], false, response => ProcessGeoSearchResponse(response, options)); } @@ -224,7 +226,7 @@ private static GeoRadiusResult[] ProcessGeoSearchResponse(object[] response, Geo { - return response.Select(item => + return [.. response.Select(item => { // If no options are specified, Redis returns simple strings (member names) if (options == GeoRadiusOptions.None) @@ -288,7 +290,7 @@ private static GeoRadiusResult[] ProcessGeoSearchResponse(object[] response, Geo } return new GeoRadiusResult(member, distance, hash, position); - }).ToArray(); + })]; } /// @@ -302,7 +304,7 @@ private static GeoRadiusResult[] ProcessGeoSearchResponse(object[] response, Geo /// When true, stores distances instead of just member names. private static void AddGeoSearchAndStoreArgs(List args, GeoSearchShape shape, long count, bool demandClosest, Order? order, bool storeDistances) { - var shapeArgs = new List(); + List shapeArgs = []; shape.AddArgs(shapeArgs); args.AddRange(shapeArgs.Select(a => a.ToGlideString())); if (count > 0) @@ -340,7 +342,7 @@ public static Cmd GeoSearchAndStoreAsync(ValkeyKey sourceKey, Valkey { List args = [destinationKey.ToGlideString(), sourceKey.ToGlideString(), ValkeyLiterals.FROMMEMBER.ToGlideString(), fromMember.ToGlideString()]; AddGeoSearchAndStoreArgs(args, shape, count, demandClosest, order, storeDistances); - return Simple(RequestType.GeoSearchStore, args.ToArray()); + return Simple(RequestType.GeoSearchStore, [.. args]); } /// @@ -359,6 +361,6 @@ public static Cmd GeoSearchAndStoreAsync(ValkeyKey sourceKey, Valkey { List args = [destinationKey.ToGlideString(), sourceKey.ToGlideString(), ValkeyLiterals.FROMLONLAT.ToGlideString(), fromPosition.Longitude.ToGlideString(), fromPosition.Latitude.ToGlideString()]; AddGeoSearchAndStoreArgs(args, shape, count, demandClosest, order, storeDistances); - return Simple(RequestType.GeoSearchStore, args.ToArray()); + return Simple(RequestType.GeoSearchStore, [.. args]); } } From 4ad2575e6beaf9ab53df80a962a060569e77f2ee Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Mon, 20 Oct 2025 09:57:52 -0700 Subject: [PATCH 27/27] Format Signed-off-by: Alex Rehnby-Martin --- .../BatchTestUtils.cs | 15 ++++--- .../GeospatialCommandTests.cs | 39 +++++++------------ 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs b/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs index 6b2e4102..84e5d6b3 100644 --- a/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs +++ b/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs @@ -1,6 +1,6 @@ -// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + -using Valkey.Glide.Commands.Options; namespace Valkey.Glide.IntegrationTests; @@ -607,8 +607,7 @@ public static List CreateSortedSetTest(Pipeline.IBatch batch, bool isA testData.Add(new(true, "SortedSetAdd(key1, member1, 10.5)")); // Test multiple members add - SortedSetEntry[] entries = - [ + SortedSetEntry[] entries = [ new("member2", 8.2), new("member3", 15.0) ]; @@ -1405,10 +1404,10 @@ public static List CreateGeospatialTest(Pipeline.IBatch batch, bool is _ = batch.GeoAdd(key1, new GeoEntry(13.361389, 38.115556, "Palermo")); testData.Add(new(1L, "GeoAdd(key1, Palermo)")); - _ = batch.GeoAdd(key1, new GeoEntry[] { + _ = batch.GeoAdd(key1, [ new GeoEntry(15.087269, 37.502669, "Catania"), new GeoEntry(12.496366, 41.902782, "Rome") - }); + ]); testData.Add(new(2L, "GeoAdd(key1, [Catania, Rome])")); // Test GeoAdd with options @@ -1432,14 +1431,14 @@ public static List CreateGeospatialTest(Pipeline.IBatch batch, bool is _ = batch.GeoHash(key1, "Palermo"); testData.Add(new("", "GeoHash(key1, Palermo)", true)); - _ = batch.GeoHash(key1, new ValkeyValue[] { "Palermo", "Catania" }); + _ = batch.GeoHash(key1, ["Palermo", "Catania"]); testData.Add(new(Array.Empty(), "GeoHash(key1, [Palermo, Catania])", true)); // Test GeoPosition _ = batch.GeoPosition(key1, "Palermo"); testData.Add(new(new GeoPosition(13.361389, 38.115556), "GeoPosition(key1, Palermo)", true)); - _ = batch.GeoPosition(key1, new ValkeyValue[] { "Palermo", "NonExistent" }); + _ = batch.GeoPosition(key1, ["Palermo", "NonExistent"]); testData.Add(new(Array.Empty(), "GeoPosition(key1, [Palermo, NonExistent])", true)); // Test GeoSearch diff --git a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs index acab1b90..b10037d7 100644 --- a/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/GeospatialCommandTests.cs @@ -247,8 +247,7 @@ public async Task GeoDistance_DefaultUnit_ReturnsMeters(BaseClient client) public async Task GeoSearch_AllUnits_ReturnsConsistentResults(BaseClient client) { string key = Guid.NewGuid().ToString(); - GeoEntry[] entries = - [ + GeoEntry[] entries = [ new GeoEntry(13.361389, 38.115556, "Palermo"), new GeoEntry(15.087269, 37.502669, "Catania"), new GeoEntry(12.758489, 38.788135, "Trapani") @@ -295,14 +294,13 @@ public async Task GeoSearch_AllUnits_ReturnsConsistentResults(BaseClient client) public async Task GeoHash_NonExistentMembers_ReturnsNulls(BaseClient client) { string key = Guid.NewGuid().ToString(); - GeoEntry[] entries = - [ + GeoEntry[] entries = [ new GeoEntry(13.361389, 38.115556, "Palermo"), new GeoEntry(15.087269, 37.502669, "Catania") ]; await client.GeoAddAsync(key, entries); - string?[] hashes = await client.GeoHashAsync(key, new ValkeyValue[] { "Palermo", "Catania", "NonExistent" }); + string?[] hashes = await client.GeoHashAsync(key, ["Palermo", "Catania", "NonExistent"]); Assert.NotNull(hashes[0]); // Palermo Assert.NotNull(hashes[1]); // Catania Assert.Null(hashes[2]); // NonExistent @@ -315,7 +313,7 @@ public async Task GeoPosition_NonExistentMembers_ReturnsNulls(BaseClient client) string key = Guid.NewGuid().ToString(); await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo"); - GeoPosition?[] positions = await client.GeoPositionAsync(key, new ValkeyValue[] { "Palermo", "NonExistent" }); + GeoPosition?[] positions = await client.GeoPositionAsync(key, ["Palermo", "NonExistent"]); Assert.NotNull(positions[0]); // Palermo Assert.Null(positions[1]); // NonExistent } @@ -409,11 +407,11 @@ public async Task GeoHash_MultipleMembers_ReturnsHashes(BaseClient client) ]; await client.GeoAddAsync(key, entries); - string?[] hashes = await client.GeoHashAsync(key, new ValkeyValue[] { "Palermo", "Catania" }); + string?[] hashes = await client.GeoHashAsync(key, ["Palermo", "Catania"]); Assert.NotNull(hashes[0]); Assert.NotNull(hashes[1]); - Assert.NotEmpty(hashes[0]); - Assert.NotEmpty(hashes[1]); + Assert.NotEmpty(hashes[0]!); + Assert.NotEmpty(hashes[1]!); } [Theory(DisableDiscoveryEnumeration = true)] @@ -445,7 +443,7 @@ public async Task GeoHash_EmptyMembers_ReturnsEmptyArray(BaseClient client) string key = Guid.NewGuid().ToString(); await client.GeoAddAsync(key, 13.361389, 38.115556, "Palermo"); - string?[] hashes = await client.GeoHashAsync(key, new ValkeyValue[] { }); + string?[] hashes = await client.GeoHashAsync(key, []); Assert.Empty(hashes); } @@ -476,7 +474,7 @@ public async Task GeoPosition_MultipleMembers_ReturnsPositions(BaseClient client ]; await client.GeoAddAsync(key, entries); - GeoPosition?[] positions = await client.GeoPositionAsync(key, new ValkeyValue[] { "Palermo", "Catania" }); + GeoPosition?[] positions = await client.GeoPositionAsync(key, ["Palermo", "Catania"]); Assert.NotNull(positions[0]); Assert.NotNull(positions[1]); Assert.True(Math.Abs(positions[0]!.Value.Longitude - 13.361389) < 0.001); @@ -694,7 +692,6 @@ public async Task GeoSearch_WithOptions_ReturnsEnrichedResults(BaseClient client Assert.NotEmpty(distResults); var palermoDist = distResults.FirstOrDefault(r => r.Member.ToString() == "Palermo"); - Assert.NotNull(palermoDist); Assert.Equal("Palermo", palermoDist.Member.ToString()); Assert.NotNull(palermoDist.Distance); // Should have distance Assert.Equal(0.0, palermoDist.Distance.Value, 1); // Distance from itself should be ~0 @@ -705,7 +702,6 @@ public async Task GeoSearch_WithOptions_ReturnsEnrichedResults(BaseClient client Assert.NotEmpty(coordResults); var palermoCoord = coordResults.FirstOrDefault(r => r.Member.ToString() == "Palermo"); - Assert.NotNull(palermoCoord); Assert.Equal("Palermo", palermoCoord.Member.ToString()); Assert.True(palermoCoord.Position.HasValue); // Should have coordinates Assert.Null(palermoCoord.Distance); // Should be null without WithDistance @@ -729,8 +725,6 @@ public async Task GeoSearch_WithDistance_ReturnsAccurateDistances(BaseClient cli var palermoResult = results.FirstOrDefault(r => r.Member.ToString() == "Palermo"); var cataniaResult = results.FirstOrDefault(r => r.Member.ToString() == "Catania"); - Assert.NotNull(palermoResult); - Assert.NotNull(cataniaResult); Assert.NotNull(palermoResult.Distance); Assert.NotNull(cataniaResult.Distance); @@ -812,13 +806,11 @@ public async Task GeoSearchAndStore_WithDistances_StoresDistances(BaseClient cli var results = await client.SortedSetRangeByRankWithScoresAsync(destinationKey, 0, -1); Assert.Equal(2, results.Length); - var palermoResult = results.FirstOrDefault(r => r.Key.ToString() == "Palermo"); - var cataniaResult = results.FirstOrDefault(r => r.Key.ToString() == "Catania"); + var palermoResult = results.FirstOrDefault(r => r.Element.ToString() == "Palermo"); + var cataniaResult = results.FirstOrDefault(r => r.Element.ToString() == "Catania"); - Assert.NotNull(palermoResult); - Assert.NotNull(cataniaResult); - Assert.Equal(0.0, palermoResult.Value, 0.1); - Assert.Equal(166.2742, cataniaResult.Value, 0.1); + Assert.Equal(0.0, palermoResult.Score, 0.1); + Assert.Equal(166.2742, cataniaResult.Score, 0.1); } [Theory(DisableDiscoveryEnumeration = true)] @@ -899,8 +891,7 @@ public async Task GeoSearchAndStore_WithCount_LimitsStoredResults(BaseClient cli string sourceKey = keyPrefix + ":source"; string destinationKey = keyPrefix + ":dest"; - GeoEntry[] entries = - [ + GeoEntry[] entries = [ new GeoEntry(13.361389, 38.115556, "Palermo"), new GeoEntry(15.087269, 37.502669, "Catania"), new GeoEntry(12.758489, 38.788135, "Trapani"), @@ -932,7 +923,7 @@ public async Task GeoSearchAndStore_OverwritesDestination(BaseClient client) ]; await client.GeoAddAsync(sourceKey, entries); - await client.SortedSetAddAsync(destinationKey, new SortedSetEntry[] { new SortedSetEntry("OldMember", 100) }); + await client.SortedSetAddAsync(destinationKey, [new SortedSetEntry("OldMember", 100)]); Assert.Equal(1, await client.SortedSetCardAsync(destinationKey)); var shape = new GeoSearchCircle(200, GeoUnit.Kilometers);