diff --git a/src/StackExchange.Redis/RedisDatabase.VectorSets.cs b/src/StackExchange.Redis/RedisDatabase.VectorSets.cs index 7b29b0da9..5732b0694 100644 --- a/src/StackExchange.Redis/RedisDatabase.VectorSets.cs +++ b/src/StackExchange.Redis/RedisDatabase.VectorSets.cs @@ -200,16 +200,9 @@ private Message GetVectorSetRangeMessage( Exclude exclude, CommandFlags flags) { - static RedisValue GetTerminator(RedisValue value, Exclude exclude, bool isStart) - { - if (value.IsNull) return isStart ? RedisLiterals.MinusSymbol : RedisLiterals.PlusSymbol; - var mask = isStart ? Exclude.Start : Exclude.Stop; - var isExclusive = (exclude & mask) != 0; - return ((isExclusive ? "(" : "[") + value).AsRedisValue(); - } - - var from = GetTerminator(start, exclude, true); - var to = GetTerminator(end, exclude, false); + // same lexicographic bounds as ZRANGEBYLEX; built from the raw bytes, so binary members are preserved + var from = GetLexRange(start, exclude, isStart: true, Order.Ascending); + var to = GetLexRange(end, exclude, isStart: false, Order.Ascending); return count < 0 ? Message.Create(Database, flags, RedisCommand.VRANGE, key, from, to) : Message.Create(Database, flags, RedisCommand.VRANGE, key, from, to, count); diff --git a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs index 3839d262f..0515451ba 100644 --- a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs +++ b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs @@ -967,6 +967,60 @@ public async Task VectorSetRange_WithNumericLexOrder() Assert.Equal(new[] { "1", "10", "2", "20", "3" }, result.Span.ToArray().Select(r => (string?)r).ToArray()); } + [Fact] + public async Task VectorSetRange_BinaryMemberAsBound() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + byte[] binary = [0xFF, 0x01]; // not valid UTF-8 + foreach (var member in new RedisValue[] { "a", "b", binary }) + { + var request = VectorSetAddRequest.Member(member, vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + } + + // nothing sorts after the binary member + using var afterBinary = await db.VectorSetRangeAsync(key, start: binary, exclude: Exclude.Start); + Assert.NotNull(afterBinary); + Assert.Equal(0, afterBinary.Length); + + using var upToBinary = await db.VectorSetRangeAsync(key, start: "b", end: binary, exclude: Exclude.Start); + Assert.NotNull(upToBinary); + Assert.Equal(1, upToBinary.Length); + Assert.Equal(binary, (byte[]?)upToBinary.Span[0]); + } + + [Fact] + public async Task VectorSetRangeEnumerate_BinaryMembers() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + byte[] binary = [0xFF, 0x01]; // not valid UTF-8 + foreach (var member in new RedisValue[] { "a", "b", binary }) + { + var request = VectorSetAddRequest.Member(member, vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + } + + // each batch starts after the last member of the previous one; Take guards against a loop + var allMembers = db.VectorSetRangeEnumerate(key, count: 1).Take(10).ToArray(); + + Assert.Equal(3, allMembers.Length); + Assert.Equal("a", (string?)allMembers[0]); + Assert.Equal("b", (string?)allMembers[1]); + Assert.Equal(binary, (byte[]?)allMembers[2]); + } + [Fact] public async Task VectorSetRangeEnumerate_BasicIteration() {