Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions src/StackExchange.Redis/Interfaces/IDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2265,7 +2265,15 @@ public partial interface IDatabase : IRedis, IDatabaseAsync
/// <param name="exclude">Whether to exclude <paramref name="min"/> and <paramref name="max"/> from the range check (defaults to both inclusive).</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>The number of elements in the specified score range.</returns>
/// <remarks><seealso href="https://redis.io/commands/zlexcount"/></remarks>
/// <remarks>
/// <para>
/// The bounds are compared by their raw bytes, the way Redis orders a lexicographical range, and are swapped if
/// <paramref name="min"/> sorts after <paramref name="max"/>, so the count matches what
/// <see cref="SortedSetRangeByValue(RedisKey, RedisValue, RedisValue, Exclude, Order, long, long, CommandFlags)"/> returns.
/// A bound that looks like a number, such as <c>"0123"</c> or <c>10</c>, is ordered as text, so <c>"10"</c> sorts before <c>"2"</c>.
/// </para>
/// <para><seealso href="https://redis.io/commands/zlexcount"/></para>
/// </remarks>
long SortedSetLengthByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None);

/// <summary>
Expand Down Expand Up @@ -2461,7 +2469,14 @@ SortedSetEntry[] SortedSetRangeByScoreWithScores(
/// <param name="take">How many items to take.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>List of elements in the specified score range.</returns>
/// <remarks><seealso href="https://redis.io/commands/zrangebylex"/></remarks>
/// <remarks>
/// <para>
/// The bounds are compared by their raw bytes, the way Redis orders a lexicographical range, and are swapped if
/// <paramref name="min"/> sorts after <paramref name="max"/> (an excluded bound stays excluded).
/// A bound that looks like a number, such as <c>"0123"</c> or <c>10</c>, is ordered as text, so <c>"10"</c> sorts before <c>"2"</c>.
/// </para>
/// <para><seealso href="https://redis.io/commands/zrangebylex"/></para>
/// </remarks>
RedisValue[] SortedSetRangeByValue(
RedisKey key,
RedisValue min,
Expand All @@ -2485,9 +2500,16 @@ RedisValue[] SortedSetRangeByValue(
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>List of elements in the specified score range.</returns>
/// <remarks>
/// <para>
/// The bounds are compared by their raw bytes, the way Redis orders a lexicographical range, and are swapped if
/// <paramref name="min"/> sorts after <paramref name="max"/> (an excluded bound stays excluded).
/// A bound that looks like a number, such as <c>"0123"</c> or <c>10</c>, is ordered as text, so <c>"10"</c> sorts before <c>"2"</c>.
/// </para>
/// <para>
/// See
/// <seealso href="https://redis.io/commands/zrangebylex"/>,
/// <seealso href="https://redis.io/commands/zrevrangebylex"/>.
/// </para>
/// </remarks>
RedisValue[] SortedSetRangeByValue(
RedisKey key,
Expand Down Expand Up @@ -2571,7 +2593,15 @@ RedisValue[] SortedSetRangeByValue(
/// <param name="exclude">Which of <paramref name="min"/> and <paramref name="max"/> to exclude (defaults to both inclusive).</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>The number of elements removed.</returns>
/// <remarks><seealso href="https://redis.io/commands/zremrangebylex"/></remarks>
/// <remarks>
/// <para>
/// Unlike <see cref="SortedSetRangeByValue(RedisKey, RedisValue, RedisValue, Exclude, Order, long, long, CommandFlags)"/>,
/// the bounds are not reordered: if <paramref name="min"/> sorts after <paramref name="max"/>, nothing is removed.
/// Redis compares them by their raw bytes, so a bound that looks like a number is ordered as text:
/// <c>"2"</c> to <c>"10"</c> removes nothing, because <c>"10"</c> sorts before <c>"2"</c>.
/// </para>
/// <para><seealso href="https://redis.io/commands/zremrangebylex"/></para>
/// </remarks>
long SortedSetRemoveRangeByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None);

/// <summary>
Expand Down
8 changes: 7 additions & 1 deletion src/StackExchange.Redis/RedisDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5848,6 +5848,7 @@ private Message GetLexMessage(RedisCommand command, RedisKey key, RedisValue min

public long SortedSetLengthByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None)
{
ReverseLimits(Order.Ascending, ref exclude, ref min, ref max); // count what SortedSetRangeByValue would return
var msg = GetLexMessage(RedisCommand.ZLEXCOUNT, key, min, max, exclude, 0, -1, flags, Order.Ascending);
return ExecuteSync(msg, ResultProcessor.Int64);
}
Expand All @@ -5857,7 +5858,8 @@ public RedisValue[] SortedSetRangeByValue(RedisKey key, RedisValue min, RedisVal

private static void ReverseLimits(Order order, ref Exclude exclude, ref RedisValue start, ref RedisValue stop)
{
bool reverseLimits = (order == Order.Ascending) == (stop != default && start.CompareTo(stop) > 0);
// order by raw bytes, as redis does; CompareTo would read numeric-looking text such as "0123" as a number
bool reverseLimits = (order == Order.Ascending) == (stop != default && RedisValue.LexCompare(start, stop) > 0);
if (reverseLimits)
{
var tmp = start;
Expand Down Expand Up @@ -5887,12 +5889,15 @@ public RedisValue[] SortedSetRangeByValue(

public long SortedSetRemoveRangeByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None)
{
// deliberately not reordered (unlike range/length): bounds the wrong way round remove nothing, rather than
// everything between them as text - "2" and "10" would otherwise also remove "15" and "100"
var msg = GetLexMessage(RedisCommand.ZREMRANGEBYLEX, key, min, max, exclude, 0, -1, flags, Order.Ascending);
return ExecuteSync(msg, ResultProcessor.Int64);
}

public Task<long> SortedSetLengthByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None)
{
ReverseLimits(Order.Ascending, ref exclude, ref min, ref max); // count what SortedSetRangeByValueAsync would return
var msg = GetLexMessage(RedisCommand.ZLEXCOUNT, key, min, max, exclude, 0, -1, flags, Order.Ascending);
return ExecuteAsync(msg, ResultProcessor.Int64);
}
Expand All @@ -5917,6 +5922,7 @@ public Task<RedisValue[]> SortedSetRangeByValueAsync(

public Task<long> SortedSetRemoveRangeByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None)
{
// deliberately not reordered; see SortedSetRemoveRangeByValue
var msg = GetLexMessage(RedisCommand.ZREMRANGEBYLEX, key, min, max, exclude, 0, -1, flags, Order.Ascending);
return ExecuteAsync(msg, ResultProcessor.Int64);
}
Expand Down
28 changes: 28 additions & 0 deletions src/StackExchange.Redis/RedisValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,34 @@ private static int BlobCompareTo(in RedisValue x, in RedisValue y)
return x.UnsafeRawSpan(out xScratch).SequenceCompareTo(y.UnsafeRawSpan(out yScratch));
}

// byte-wise ordinal comparison of any two values by their raw bytes, which is how redis orders a lex range;
// unlike CompareTo, numbers compare as their text ("10" and 10 both sort before "2", and "0123" is not 123),
// and null and empty are both no bytes, so they sort first
internal static int LexCompare(in RedisValue x, in RedisValue y)
{
try
{
if (IsBlob(x.Type) && IsBlob(y.Type)) return BlobCompareTo(x, y); // both byte-backed: no copies

byte[]? xLease = null, yLease = null;
try
{
return x.AsMemory(out xLease).Span.SequenceCompareTo(y.AsMemory(out yLease).Span);
}
finally
{
if (xLease is not null) ArrayPool<byte>.Shared.Return(xLease);
if (yLease is not null) ArrayPool<byte>.Shared.Return(yLease);
}
}
catch (Exception ex)
{
// never throws: like CompareTo, a value that can't be read as bytes is treated as equal
ConnectionMultiplexer.TraceWithoutContext(ex.Message);
return 0;
}
}

// true if 'whole' starts with the bytes of 'prefix', for any combination of byte-backed kinds
private static bool BlobStartsWith(in RedisValue whole, in RedisValue prefix)
{
Expand Down
Loading
Loading