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
1 change: 1 addition & 0 deletions .claude/skills/implement-resp-command/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Before writing anything, get the command's exact argument order and reply shape

2. **Declare the method on the interfaces** — `src/StackExchange.Redis/Interfaces/IDatabase.cs` *and* `IDatabaseAsync.cs` (or the `.Arrays.cs` / `.VectorSets.cs` partials when relevant). Always provide both sync and async.
- **Back-compat:** never add an optional parameter to an existing shipped method (binary break → `MissingMethodException`). Add a new **overload** instead; see `AGENTS.md`.
- **Additive-overload trick (avoids ambiguity):** if the shipped method has an all-optional tail (e.g. `Foo(key, int? count = null, CommandFlags flags = CommandFlags.None)`), a new overload that just appends more optional params (`Foo(key, int? count = null, int? extra = null, CommandFlags flags = ...)`) is **ambiguous** for existing calls like `Foo(key, 5)` — both candidates substitute a default, so neither is "better" (compile error `CS0121`, and the analyzer flags `RS0026`). The fix: **make the existing overload's parameters non-optional (remove the `= ...` defaults) and let the new overload carry all the optionals.** Removing a default is binary-safe (the IL signature is unchanged) and source-safe (old calls simply rebind to the new all-optional overload, which is functionally identical). Do this consistently across the interface *and* every implementor (`RedisDatabase`, `KeyPrefixed`/`KeyPrefixedDatabase`), and update the changed signatures in **`PublicAPI.Shipped.txt`** (optional→required is an edit-in-place there; the new all-optional overload goes in `PublicAPI.Unshipped.txt`). Wrap the new overload in `#pragma warning disable RS0026` since the analyzer still flags competing optional-param overloads even when they're unambiguous. Watch for the "richest" existing overload living in a sibling partial (e.g. `IDatabaseAsync.VectorSets.cs`).
- **Implement the new member on every `IDatabase`/`IDatabaseAsync` implementor**, or the build breaks. Chiefly `KeyspaceIsolation/KeyPrefixedDatabase.cs` — and there it must prefix keys via `ToInner(key)`; a stub that forwards without prefixing compiles but **silently breaks keyspace isolation** for the new command. If the command should also be usable in batches/transactions, add it to `IBatch`/`ITransaction` and their implementations (`RedisBatch`/`RedisTransaction`/`KeyPrefixedBatch`) too.

3. **Implement in `RedisDatabase.cs`** (next to the template you picked). The standard shape:
Expand Down
32 changes: 32 additions & 0 deletions src/StackExchange.Redis/Enums/ListMoveCount.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;

namespace StackExchange.Redis;

/// <summary>
/// Specifies how the requested count is interpreted when moving multiple list elements via
/// <c>LMOVEM</c> (see <see cref="IDatabase.ListMove(RedisKey, RedisKey, ListSide, ListSide, long, ListMoveCount, ListMoveOrder, CommandFlags)"/>).
/// </summary>
public enum ListMoveCount
{
/// <summary>
/// Move <em>up to</em> the requested number of elements (<c>COUNT</c>); fewer are moved when the
/// source list has fewer elements, and a null result is returned when the source is empty.
/// </summary>
UpTo,

/// <summary>
/// Move <em>exactly</em> the requested number of elements (<c>EXACTLY</c>); if the source list does
/// not have that many elements, nothing is moved and a null result is returned.
/// </summary>
Exactly,
}

internal static class ListMoveCountExtensions
{
internal static RedisValue ToLiteral(this ListMoveCount mode) => mode switch
{
ListMoveCount.UpTo => RedisLiterals.COUNT,
ListMoveCount.Exactly => RedisLiterals.EXACTLY,
_ => throw new ArgumentOutOfRangeException(nameof(mode)),
};
}
31 changes: 31 additions & 0 deletions src/StackExchange.Redis/Enums/ListMoveOrder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;

namespace StackExchange.Redis;

/// <summary>
/// Specifies the ordering of the elements moved (and returned) when moving multiple list elements via
/// <c>LMOVEM</c> (see <see cref="IDatabase.ListMove(RedisKey, RedisKey, ListSide, ListSide, long, ListMoveCount, ListMoveOrder, CommandFlags)"/>).
/// </summary>
public enum ListMoveOrder
{
/// <summary>
/// Move the elements as a single block (<c>BULK</c>), preserving the source list's relative order.
/// </summary>
Bulk,

/// <summary>
/// Move the elements one-by-one (<c>OBO</c>), as if each was popped from the source and pushed to the
/// destination individually.
/// </summary>
OneByOne,
}

internal static class ListMoveOrderExtensions
{
internal static RedisValue ToLiteral(this ListMoveOrder order) => order switch
{
ListMoveOrder.Bulk => RedisLiterals.BULK,
ListMoveOrder.OneByOne => RedisLiterals.OBO,
_ => throw new ArgumentOutOfRangeException(nameof(order)),
};
}
6 changes: 6 additions & 0 deletions src/StackExchange.Redis/Enums/RedisCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ internal enum RedisCommand
LINSERT,
LLEN,
LMOVE,
LMOVEM,
LMPOP,
LPOP,
LPOS,
Expand Down Expand Up @@ -191,6 +192,7 @@ internal enum RedisCommand
SCARD,
SCRIPT,
SDIFF,
SDIFFCARD,
SDIFFSTORE,
SELECT,
SENTINEL,
Expand Down Expand Up @@ -219,6 +221,7 @@ internal enum RedisCommand
STRLEN,
SUBSCRIBE,
SUNION,
SUNIONCARD,
SUNIONSTORE,
SSCAN,
SSUBSCRIBE,
Expand Down Expand Up @@ -378,6 +381,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
case RedisCommand.INCREX:
case RedisCommand.LINSERT:
case RedisCommand.LMOVE:
case RedisCommand.LMOVEM:
case RedisCommand.LMPOP:
case RedisCommand.LPOP:
case RedisCommand.LPUSH:
Expand Down Expand Up @@ -529,6 +533,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
case RedisCommand.SCARD:
case RedisCommand.SCRIPT:
case RedisCommand.SDIFF:
case RedisCommand.SDIFFCARD:
case RedisCommand.SELECT:
case RedisCommand.SENTINEL:
case RedisCommand.SHUTDOWN:
Expand All @@ -546,6 +551,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
case RedisCommand.STRLEN:
case RedisCommand.SUBSCRIBE:
case RedisCommand.SUNION:
case RedisCommand.SUNIONCARD:
case RedisCommand.SUNSUBSCRIBE:
case RedisCommand.SSCAN:
case RedisCommand.SYNC:
Expand Down
8 changes: 8 additions & 0 deletions src/StackExchange.Redis/Enums/SetOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ internal static class SetOperationExtensions
_ => OutOfRange(operation),
};

internal static RedisCommand ToSetCardinalityCommand(this SetOperation operation) => operation switch
{
SetOperation.Union => RedisCommand.SUNIONCARD,
SetOperation.Intersect => RedisCommand.SINTERCARD,
SetOperation.Difference => RedisCommand.SDIFFCARD,
_ => OutOfRange(operation),
};

internal static RedisCommand ToSortedSetCommand(this SetOperation operation) => operation switch
{
SetOperation.Union => RedisCommand.ZUNION,
Expand Down
103 changes: 100 additions & 3 deletions src/StackExchange.Redis/Interfaces/IDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1370,6 +1370,28 @@ public partial interface IDatabase : IRedis, IDatabaseAsync
/// <remarks><seealso href="https://redis.io/commands/lmove"/></remarks>
RedisValue ListMove(RedisKey sourceKey, RedisKey destinationKey, ListSide sourceSide, ListSide destinationSide, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Removes up to (or exactly) <paramref name="count"/> elements from the first or last of the list stored at
/// <paramref name="sourceKey"/>, and pushes them to the first or last of the list stored at <paramref name="destinationKey"/>,
/// returning the moved elements in destination order.
/// </summary>
/// <param name="sourceKey">The key of the list to remove from.</param>
/// <param name="destinationKey">The key of the list to move to.</param>
/// <param name="sourceSide">What side of the <paramref name="sourceKey"/> list to remove from.</param>
/// <param name="destinationSide">What side of the <paramref name="destinationKey"/> list to move to.</param>
/// <param name="count">The number of elements to move.</param>
/// <param name="mode">Whether <paramref name="count"/> is an upper bound (<c>COUNT</c>) or an exact requirement (<c>EXACTLY</c>).</param>
/// <param name="order">Whether the elements are moved as a single block (<c>BULK</c>) or one-by-one (<c>OBO</c>).</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>
/// The elements moved, in destination order; an empty array if there was nothing to move; or <see langword="null"/>
/// when <paramref name="mode"/> is <see cref="ListMoveCount.Exactly"/> and the source list does not have <paramref name="count"/> elements.
/// </returns>
/// <remarks><seealso href="https://redis.io/commands/lmovem"/></remarks>
#pragma warning disable RS0026 // competing overloads - disambiguated by the required count parameter
RedisValue[]? ListMove(RedisKey sourceKey, RedisKey destinationKey, ListSide sourceSide, ListSide destinationSide, long count, ListMoveCount mode = ListMoveCount.UpTo, ListMoveOrder order = ListMoveOrder.Bulk, CommandFlags flags = CommandFlags.None);
#pragma warning restore RS0026

/// <summary>
/// Returns the specified elements of the list stored at key.
/// The offsets start and stop are zero-based indexes, with 0 being the first element of the list (the head of the list), 1 being the next element and so on.
Expand Down Expand Up @@ -1791,9 +1813,37 @@ public partial interface IDatabase : IRedis, IDatabaseAsync
/// <param name="limit">The number of elements to check (defaults to 0 and means unlimited).</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>The cardinality (number of elements) of the set, or 0 if key does not exist.</returns>
/// <remarks><seealso href="https://redis.io/commands/scard"/></remarks>
/// <remarks><seealso href="https://redis.io/commands/sintercard"/></remarks>
long SetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None);

/// <summary>
/// <para>
/// Returns the set cardinality (number of elements) of the set that would result from combining the sets
/// stored at the given <paramref name="keys"/> with the specified <paramref name="operation"/>, without
/// materializing the combined set or returning its members.
/// </para>
/// <para>
/// If the cardinality reaches <paramref name="limit"/> partway through the computation, the algorithm
/// will exit and yield <paramref name="limit"/> as the cardinality.
/// </para>
/// </summary>
/// <param name="operation">The operation used to combine the sets (maps to <c>SUNIONCARD</c>, <c>SINTERCARD</c> or <c>SDIFFCARD</c>).</param>
/// <param name="keys">The keys of the sets.</param>
/// <param name="limit">The number of elements to check (defaults to 0 and means unlimited).</param>
/// <param name="approximate">
/// When <see langword="true"/>, appends <c>APPROX</c> to request a HyperLogLog-based estimate rather than an
/// exact count. At the time of writing only <see cref="SetOperation.Union"/> (<c>SUNIONCARD</c>) accepts this;
/// it is passed through for any operation and the server will error if it is not supported.
/// </param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>The cardinality (number of elements) of the combined set.</returns>
/// <remarks>
/// <seealso href="https://redis.io/commands/sunioncard"/>,
/// <seealso href="https://redis.io/commands/sintercard"/>,
/// <seealso href="https://redis.io/commands/sdiffcard"/>.
/// </remarks>
long SetCombineLength(SetOperation operation, RedisKey[] keys, long limit = 0, bool approximate = false, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Returns the set cardinality (number of elements) of the set stored at key.
/// </summary>
Expand Down Expand Up @@ -3040,7 +3090,28 @@ IEnumerable<SortedSetEntry> SortedSetScan(
/// <para>Equivalent of calling <c>XREAD COUNT num STREAMS key1 key2 id1 id2</c>.</para>
/// <para><seealso href="https://redis.io/commands/xread"/></para>
/// </remarks>
RedisStream[] StreamRead(StreamPosition[] streamPositions, int? countPerStream = null, CommandFlags flags = CommandFlags.None);
RedisStream[] StreamRead(StreamPosition[] streamPositions, int? countPerStream, CommandFlags flags);

/// <summary>
/// Read from multiple streams, applying global caps on the total number of entries and/or the total reply size
/// across all streams (in addition to the per-stream <paramref name="countPerStream"/>).
/// </summary>
/// <param name="streamPositions">Array of streams and the positions from which to begin reading for each stream.</param>
/// <param name="countPerStream">The maximum number of messages to return from each stream.</param>
/// <param name="maxCount">The maximum total number of messages to return across all streams (<c>MAXCOUNT</c>); <see langword="null"/> for no cap.</param>
/// <param name="maxSize">The maximum total reply size in bytes across all streams (<c>MAXSIZE</c>); <see langword="null"/> for no cap.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>A value of <see cref="RedisStream"/> for each stream.</returns>
/// <remarks>
/// <para>
/// Equivalent of calling <c>XREAD COUNT num MAXCOUNT num MAXSIZE num STREAMS key1 key2 id1 id2</c>.
/// <c>MAXCOUNT</c>/<c>MAXSIZE</c> require server 8.10 or above; at least one entry is always returned even if it exceeds <paramref name="maxSize"/>.
/// </para>
/// <para><seealso href="https://redis.io/commands/xread"/></para>
/// </remarks>
#pragma warning disable RS0026 // additive overload: countPerStream is required here, so shorter calls still bind to the existing overload
RedisStream[] StreamRead(StreamPosition[] streamPositions, int? countPerStream = null, int? maxCount = null, int? maxSize = null, CommandFlags flags = CommandFlags.None);
#pragma warning restore RS0026

/// <summary>
/// Read messages from a stream into an associated consumer group.
Expand Down Expand Up @@ -3133,7 +3204,33 @@ IEnumerable<SortedSetEntry> SortedSetScan(
/// <para>Equivalent of calling <c>XREADGROUP GROUP groupName consumerName COUNT countPerStream STREAMS stream1 stream2 id1 id2</c>.</para>
/// <para><seealso href="https://redis.io/commands/xreadgroup"/></para>
/// </remarks>
RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, TimeSpan? claimMinIdleTime = null, CommandFlags flags = CommandFlags.None);
RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, bool noAck, TimeSpan? claimMinIdleTime, CommandFlags flags);

/// <summary>
/// Read from multiple streams into the given consumer group, applying global caps on the total number of entries
/// and/or the total reply size across all streams (in addition to the per-stream <paramref name="countPerStream"/>).
/// The consumer group with the given <paramref name="groupName"/> will need to have been created for each stream prior to calling this method.
/// </summary>
/// <param name="streamPositions">Array of streams and the positions from which to begin reading for each stream.</param>
/// <param name="groupName">The name of the consumer group.</param>
/// <param name="consumerName">The name of the consumer.</param>
/// <param name="countPerStream">The maximum number of messages to return from each stream.</param>
/// <param name="noAck">When true, the message will not be added to the pending message list.</param>
/// <param name="claimMinIdleTime">Auto-claim messages that have been idle for at least this long.</param>
/// <param name="maxCount">The maximum total number of messages to return across all streams (<c>MAXCOUNT</c>); <see langword="null"/> for no cap.</param>
/// <param name="maxSize">The maximum total reply size in bytes across all streams (<c>MAXSIZE</c>); <see langword="null"/> for no cap.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>A value of <see cref="RedisStream"/> for each stream.</returns>
/// <remarks>
/// <para>
/// Equivalent of calling <c>XREADGROUP GROUP groupName consumerName COUNT countPerStream MAXCOUNT num MAXSIZE num STREAMS stream1 stream2 id1 id2</c>.
/// <c>MAXCOUNT</c>/<c>MAXSIZE</c> require server 8.10 or above; at least one entry is always returned even if it exceeds <paramref name="maxSize"/>.
/// </para>
/// <para><seealso href="https://redis.io/commands/xreadgroup"/></para>
/// </remarks>
#pragma warning disable RS0026 // additive overload: countPerStream/noAck/claimMinIdleTime are required here, so shorter calls still bind to the existing overloads
RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, TimeSpan? claimMinIdleTime = null, int? maxCount = null, int? maxSize = null, CommandFlags flags = CommandFlags.None);
#pragma warning restore RS0026

/// <summary>
/// Trim the stream to a specified maximum length.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,4 @@ System.Collections.Generic.IAsyncEnumerable<RedisValue> VectorSetRangeEnumerateA
long count = 100,
Exclude exclude = Exclude.None,
CommandFlags flags = CommandFlags.None);

/// <inheritdoc cref="IDatabase.StreamReadGroup(StreamPosition[], RedisValue, RedisValue, int?, bool, TimeSpan?, CommandFlags)"/>
Task<RedisStream[]> StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, TimeSpan? claimMinIdleTime = null, CommandFlags flags = CommandFlags.None);
}
Loading
Loading