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 docs/CommandCoverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Statuses: `policy` (excluded by design), `not-applicable` (not meaningful for a
| `BLMOVE` | 6.2.0 | `policy` | Blocking; `ListMove` covers `LMOVE`. |
| `BLMOVEM` | 8.10.0 | `policy` | Blocking; the non-blocking `LMOVEM` is implemented. |
| `BLMPOP` | 7.0.0 | `policy` | Blocking; `ListLeftPop`/`ListRightPop` cover `LMPOP`. |
| `BZMOVE` | 8.12.0 | `policy` | Blocking; `SortedSetMove` covers `ZMOVE` (the `MIN`/`MAX` forms `BZMOVE` offers). |
| `BZMPOP` | 7.0.0 | `policy` | Blocking; `SortedSetPop` covers `ZMPOP`. |
| `BZPOPMAX` | 5.0.0 | `policy` | Blocking; `SortedSetPop` covers `ZPOPMAX`. |
| `BZPOPMIN` | 5.0.0 | `policy` | Blocking; `SortedSetPop` covers `ZPOPMIN`. |
Expand Down
4 changes: 4 additions & 0 deletions docs/exp/SER011.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ New features in Redis 8.12:
equivalents)
- Per-key eviction protection (`BLESS`), surfaced as `IDatabase.KeyBless`/`KeyUnbless`/`KeyBlessFlags` (and async
equivalents) with the `BlessFlags` enum, and `IServer.BlessedKeys`/`BlessedKeysAsync` for enumeration
- Atomic sorted-set moves (`ZMOVE`, `ZMOVEMEMBER`; 8.12-rc3 onwards), surfaced as `IDatabase.SortedSetMove` (from the
low or high end), `SortedSetMoveByScore` (a score range) and `SortedSetMoveMembers` (named members, with an optional
`When` condition on the destination), and async equivalents; each returns the moved entries, or `null` if nothing
moved. The blocking `BZMOVE` is not exposed, as with every blocking command

The corresponding library features must also be considered subject to change:

Expand Down
7 changes: 7 additions & 0 deletions eng/StackExchange.Redis.Build/TransactionAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,13 @@ private static (string Suggestion, ServerVersion MinVersion, string CoveredFirst
return ("SetMove[Async](source, destination, value)", ServerVersion.Any, "key,value", "key,value");
}

// Deliberately NOT mapped: SortedSetRemove + SortedSetAdd of the same member is not ZMOVEMEMBER
// (SortedSetMoveMembers, 8.12), however much it looks like the SMOVE pair above. A set member has no
// payload, so "remove here, add there" is the whole move; a sorted-set member has a score. The
// transaction writes the score the caller supplied, and adds the member whether or not the source had
// it; ZMOVEMEMBER carries the source's score across, and moves nothing when the source lacks the
// member. Same shape, different operation.

return null;

static bool SameKey(QueuedOperation a, QueuedOperation b)
Expand Down
2 changes: 2 additions & 0 deletions src/StackExchange.Redis/Enums/CommandFlags.Category.cs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ static CommandFlags DefaultCategory(RedisCommand command)
case RedisCommand.ZPOPMIN:
case RedisCommand.ZPOPMAX:
case RedisCommand.ZMPOP:
case RedisCommand.ZMOVE: // a replay moves further members (MIN/MAX), or finds the first batch already gone
case RedisCommand.ZMOVEMEMBER:
case RedisCommand.XADD: // explicit ids and IDMP/IDMPAUTO are demoted where we can see the args
case RedisCommand.PFMERGE: // destination accumulates union each call
case RedisCommand.BITFIELD: // INCRBY sub-ops compound; demoted to last-wins when the payload has none
Expand Down
4 changes: 4 additions & 0 deletions src/StackExchange.Redis/Enums/RedisCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,8 @@ internal enum RedisCommand
ZINTERCARD,
ZINTERSTORE,
ZLEXCOUNT,
ZMOVE,
ZMOVEMEMBER,
ZMPOP,
ZMSCORE,
ZPOPMAX,
Expand Down Expand Up @@ -455,6 +457,8 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
case RedisCommand.ZDIFFSTORE:
case RedisCommand.ZINTERSTORE:
case RedisCommand.ZINCRBY:
case RedisCommand.ZMOVE:
case RedisCommand.ZMOVEMEMBER:
case RedisCommand.ZMPOP:
case RedisCommand.ZPOPMAX:
case RedisCommand.ZPOPMIN:
Expand Down
64 changes: 64 additions & 0 deletions src/StackExchange.Redis/Interfaces/IDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2909,6 +2909,70 @@ IEnumerable<SortedSetEntry> SortedSetScan(
/// <remarks><seealso href="https://redis.io/commands/zmpop"/></remarks>
SortedSetPopResult SortedSetPop(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Atomically moves entries from the low-score or high-score end of the sorted set at <paramref name="sourceKey"/>
/// to the sorted set at <paramref name="destinationKey"/>, keeping their scores. A source that becomes empty is
/// deleted; an entry that already exists in the destination takes the score from the source.
/// </summary>
/// <param name="sourceKey">The key of the sorted set to move entries from.</param>
/// <param name="destinationKey">The key of the sorted set to move entries to; it is created if it does not exist.</param>
/// <param name="order"><see cref="Order.Ascending"/> moves the lowest scores first (<c>MIN</c>); <see cref="Order.Descending"/> moves the highest scores first (<c>MAX</c>).</param>
/// <param name="count">The maximum number of entries to move; when omitted no <c>COUNT</c> is sent, and the server moves one entry.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>The entries moved, in the order they were taken, or <see langword="null"/> if nothing was moved.</returns>
/// <remarks>
/// <para>Both keys must be in the same hash slot on a cluster.</para>
/// <para>The blocking form, <c>BZMOVE</c>, is not exposed: see <see href="https://seredis.dev/CommandCoverage"/>.</para>
/// <seealso href="https://redis.io/commands/zmove"/>
/// </remarks>
[Experimental(Experiments.Server_8_12, UrlFormat = Experiments.UrlFormat)]
SortedSetEntry[]? SortedSetMove(RedisKey sourceKey, RedisKey destinationKey, Order order = Order.Ascending, long? count = null, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Atomically moves the entries with a score between <paramref name="start"/> and <paramref name="stop"/> (inclusive by default),
/// lowest score first, from the sorted set at <paramref name="sourceKey"/> to the sorted set at <paramref name="destinationKey"/>,
/// keeping their scores. A source that becomes empty is deleted; an entry that already exists in the destination takes
/// the score from the source.
/// </summary>
/// <param name="sourceKey">The key of the sorted set to move entries from.</param>
/// <param name="destinationKey">The key of the sorted set to move entries to; it is created if it does not exist.</param>
/// <param name="start">The minimum score to move.</param>
/// <param name="stop">The maximum score to move.</param>
/// <param name="exclude">Which of <paramref name="start"/> and <paramref name="stop"/> to exclude (defaults to both inclusive).</param>
/// <param name="count">
/// The maximum number of entries to move. When omitted no <c>COUNT</c> is sent, and the server moves <b>every</b> entry in
/// the range in a single command; on a large range that occupies the server for the duration.
/// </param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>The entries moved, lowest score first, or <see langword="null"/> if nothing was moved.</returns>
/// <remarks>
/// <para>Both keys must be in the same hash slot on a cluster.</para>
/// <seealso href="https://redis.io/commands/zmove"/>
/// </remarks>
[Experimental(Experiments.Server_8_12, UrlFormat = Experiments.UrlFormat)]
SortedSetEntry[]? SortedSetMoveByScore(RedisKey sourceKey, RedisKey destinationKey, double start, double stop, Exclude exclude = Exclude.None, long? count = null, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Atomically moves the specified <paramref name="members"/> from the sorted set at <paramref name="sourceKey"/> to the
/// sorted set at <paramref name="destinationKey"/>, keeping their scores. A source that becomes empty is deleted.
/// </summary>
/// <param name="sourceKey">The key of the sorted set to move members from.</param>
/// <param name="destinationKey">The key of the sorted set to move members to; it is created if it does not exist.</param>
/// <param name="members">The members to move; a member that is not in the source is skipped, and a member given twice moves once.</param>
/// <param name="when">
/// The condition on the <b>destination</b>, evaluated per member: <see cref="When.Always"/> moves the member and overwrites any
/// score it has in the destination; <see cref="When.NotExists"/> (<c>NX</c>) moves it only if the destination does not have it;
/// <see cref="When.Exists"/> (<c>XX</c>) moves it only if the destination already has it. A member that fails the condition stays in the source.
/// </param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>The entries moved, in the order of <paramref name="members"/>, or <see langword="null"/> if nothing was moved.</returns>
/// <remarks>
/// <para>Both keys must be in the same hash slot on a cluster.</para>
/// <seealso href="https://redis.io/commands/zmovemember"/>
/// </remarks>
[Experimental(Experiments.Server_8_12, UrlFormat = Experiments.UrlFormat)]
SortedSetEntry[]? SortedSetMoveMembers(RedisKey sourceKey, RedisKey destinationKey, RedisValue[] members, When when = When.Always, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Same as <see cref="SortedSetAdd(RedisKey, SortedSetEntry[], SortedSetWhen, CommandFlags)" /> but return the number of the elements changed.
/// </summary>
Expand Down
12 changes: 12 additions & 0 deletions src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,18 @@ IAsyncEnumerable<SortedSetEntry> SortedSetScanAsync(
/// <inheritdoc cref="IDatabase.SortedSetPop(RedisKey[], long, Order, CommandFlags)"/>
Task<SortedSetPopResult> SortedSetPopAsync(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None);

/// <inheritdoc cref="IDatabase.SortedSetMove(RedisKey, RedisKey, Order, long?, CommandFlags)"/>
[Experimental(Experiments.Server_8_12, UrlFormat = Experiments.UrlFormat)]
Task<SortedSetEntry[]?> SortedSetMoveAsync(RedisKey sourceKey, RedisKey destinationKey, Order order = Order.Ascending, long? count = null, CommandFlags flags = CommandFlags.None);

/// <inheritdoc cref="IDatabase.SortedSetMoveByScore(RedisKey, RedisKey, double, double, Exclude, long?, CommandFlags)"/>
[Experimental(Experiments.Server_8_12, UrlFormat = Experiments.UrlFormat)]
Task<SortedSetEntry[]?> SortedSetMoveByScoreAsync(RedisKey sourceKey, RedisKey destinationKey, double start, double stop, Exclude exclude = Exclude.None, long? count = null, CommandFlags flags = CommandFlags.None);

/// <inheritdoc cref="IDatabase.SortedSetMoveMembers(RedisKey, RedisKey, RedisValue[], When, CommandFlags)"/>
[Experimental(Experiments.Server_8_12, UrlFormat = Experiments.UrlFormat)]
Task<SortedSetEntry[]?> SortedSetMoveMembersAsync(RedisKey sourceKey, RedisKey destinationKey, RedisValue[] members, When when = When.Always, CommandFlags flags = CommandFlags.None);

/// <inheritdoc cref="IDatabase.StreamAcknowledge(RedisKey, RedisValue, RedisValue, CommandFlags)"/>
Task<long> StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None);

Expand Down
9 changes: 9 additions & 0 deletions src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,15 @@ public Task<SortedSetEntry[]> SortedSetPopAsync(RedisKey key, long count, Order
public Task<SortedSetPopResult> SortedSetPopAsync(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) =>
Inner.SortedSetPopAsync(ToInner(keys), count, order, flags);

public Task<SortedSetEntry[]?> SortedSetMoveAsync(RedisKey sourceKey, RedisKey destinationKey, Order order = Order.Ascending, long? count = null, CommandFlags flags = CommandFlags.None) =>
Inner.SortedSetMoveAsync(ToInner(sourceKey), ToInner(destinationKey), order, count, flags);

public Task<SortedSetEntry[]?> SortedSetMoveByScoreAsync(RedisKey sourceKey, RedisKey destinationKey, double start, double stop, Exclude exclude = Exclude.None, long? count = null, CommandFlags flags = CommandFlags.None) =>
Inner.SortedSetMoveByScoreAsync(ToInner(sourceKey), ToInner(destinationKey), start, stop, exclude, count, flags);

public Task<SortedSetEntry[]?> SortedSetMoveMembersAsync(RedisKey sourceKey, RedisKey destinationKey, RedisValue[] members, When when = When.Always, CommandFlags flags = CommandFlags.None) =>
Inner.SortedSetMoveMembersAsync(ToInner(sourceKey), ToInner(destinationKey), members, when, flags);

public Task<long> StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None) =>
Inner.StreamAcknowledgeAsync(ToInner(key), groupName, messageId, flags);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,15 @@ public SortedSetEntry[] SortedSetPop(RedisKey key, long count, Order order = Ord
public SortedSetPopResult SortedSetPop(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) =>
Inner.SortedSetPop(ToInner(keys), count, order, flags);

public SortedSetEntry[]? SortedSetMove(RedisKey sourceKey, RedisKey destinationKey, Order order = Order.Ascending, long? count = null, CommandFlags flags = CommandFlags.None) =>
Inner.SortedSetMove(ToInner(sourceKey), ToInner(destinationKey), order, count, flags);

public SortedSetEntry[]? SortedSetMoveByScore(RedisKey sourceKey, RedisKey destinationKey, double start, double stop, Exclude exclude = Exclude.None, long? count = null, CommandFlags flags = CommandFlags.None) =>
Inner.SortedSetMoveByScore(ToInner(sourceKey), ToInner(destinationKey), start, stop, exclude, count, flags);

public SortedSetEntry[]? SortedSetMoveMembers(RedisKey sourceKey, RedisKey destinationKey, RedisValue[] members, When when = When.Always, CommandFlags flags = CommandFlags.None) =>
Inner.SortedSetMoveMembers(ToInner(sourceKey), ToInner(destinationKey), members, when, flags);

public long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None) =>
Inner.StreamAcknowledge(ToInner(key), groupName, messageId, flags);

Expand Down
7 changes: 7 additions & 0 deletions src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ StackExchange.Redis.RedisFeatures.SetMemberExpiry.get -> bool
StackExchange.Redis.KeyNotificationType.Bless = 66 -> StackExchange.Redis.KeyNotificationType
StackExchange.Redis.KeyNotificationType.Unbless = 67 -> StackExchange.Redis.KeyNotificationType
StackExchange.Redis.RedisFeatures.Bless.get -> bool
[SER011]StackExchange.Redis.IDatabase.SortedSetMove(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[]?
[SER011]StackExchange.Redis.IDatabase.SortedSetMoveByScore(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, double start, double stop, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, long? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[]?
[SER011]StackExchange.Redis.IDatabase.SortedSetMoveMembers(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.RedisValue[]! members, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[]?
[SER011]StackExchange.Redis.IDatabaseAsync.SortedSetMoveAsync(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.SortedSetEntry[]?>!
[SER011]StackExchange.Redis.IDatabaseAsync.SortedSetMoveByScoreAsync(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, double start, double stop, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, long? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.SortedSetEntry[]?>!
[SER011]StackExchange.Redis.IDatabaseAsync.SortedSetMoveMembersAsync(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.RedisValue[]! members, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.SortedSetEntry[]?>!
StackExchange.Redis.RedisFeatures.SortedSetMove.get -> bool
[SER012]StackExchange.Redis.ClusterSlotStatsEntry
[SER012]StackExchange.Redis.ClusterSlotStatsEntry.CpuTime.get -> System.TimeSpan?
[SER012]StackExchange.Redis.ClusterSlotStatsEntry.KeyCount.get -> long
Expand Down
Loading
Loading