Skip to content

Support COPY #2064

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Apr 8, 2022
1 change: 1 addition & 0 deletions docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Fixes a few internal edge cases that will now throw proper errors (rather than a downstream null reference)
- Fixes inconsistencies with `null` vs. empty array returns (preferring an not-null empty array in those edge cases)
- Note: does *not* increment a major version (as these are warnings to consumers), because: they're warnings (errors are opt-in), removing obsolete types with a 3.0 rev _would_ be binary breaking (this isn't), and reving to 3.0 would cause binding redirect pain for consumers. Bumping from 2.5 to 2.6 only for this change.
- Adds: Support for `COPY` ([#2064 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2064))

## 2.5.61

Expand Down
1 change: 1 addition & 0 deletions src/StackExchange.Redis/Enums/RedisCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal enum RedisCommand
CLIENT,
CLUSTER,
CONFIG,
COPY,

DBSIZE,
DEBUG,
Expand Down
12 changes: 12 additions & 0 deletions src/StackExchange.Redis/Interfaces/IDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,18 @@ public interface IDatabase : IRedis, IDatabaseAsync
/// <returns>The endpoint serving the key.</returns>
EndPoint? IdentifyEndpoint(RedisKey key = default, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Copies the value from the <paramref name="sourceKey"/> to the specified <paramref name="destinationKey"/>.
/// </summary>
/// <param name="sourceKey">The key of the source value to copy.</param>
/// <param name="destinationKey">The destination key to copy the source to.</param>
/// <param name="destinationDatabase">The database ID to store <paramref name="destinationKey"/> in. If default (-1), current database is used.</param>
/// <param name="replace">Whether to overwrite an existing values at <paramref name="destinationKey"/>. If <see langword="false"/> and the key exists, the copy will not succeed.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns><see langword="true"/> if key was copied. <see langword="false"/> if key was not copied.</returns>
/// <remarks>https://redis.io/commands/copy</remarks>
bool KeyCopy(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Removes the specified key. A key is ignored if it does not exist.
/// If UNLINK is available (Redis 4.0+), it will be used.
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 @@ -448,6 +448,18 @@ public interface IDatabaseAsync : IRedisAsync
/// <returns>The endpoint serving the key.</returns>
Task<EndPoint?> IdentifyEndpointAsync(RedisKey key = default, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Copies the value from the <paramref name="sourceKey"/> to the specified <paramref name="destinationKey"/>.
/// </summary>
/// <param name="sourceKey">The key of the source value to copy.</param>
/// <param name="destinationKey">The destination key to copy the source to.</param>
/// <param name="destinationDatabase">The database ID to store <paramref name="destinationKey"/> in. If default (-1), current database is used.</param>
/// <param name="replace">Whether to overwrite an existing values at <paramref name="destinationKey"/>. If <see langword="false"/> and the key exists, the copy will not succeed.</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns><see langword="true"/> if key was copied. <see langword="false"/> if key was not copied.</returns>
/// <remarks>https://redis.io/commands/copy</remarks>
Task<bool> KeyCopyAsync(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Removes the specified key. A key is ignored if it does not exist.
/// If UNLINK is available (Redis 4.0+), it will be used.
Expand Down
3 changes: 3 additions & 0 deletions src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ public void HyperLogLogMerge(RedisKey destination, RedisKey first, RedisKey seco
public EndPoint? IdentifyEndpoint(RedisKey key = default, CommandFlags flags = CommandFlags.None) =>
Inner.IdentifyEndpoint(ToInner(key), flags);

public bool KeyCopy(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None) =>
Inner.KeyCopy(ToInner(sourceKey), ToInner(destinationKey), destinationDatabase, replace, flags);

public long KeyDelete(RedisKey[] keys, CommandFlags flags = CommandFlags.None) =>
Inner.KeyDelete(ToInner(keys), flags);

Expand Down
3 changes: 3 additions & 0 deletions src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ public Task HyperLogLogMergeAsync(RedisKey destination, RedisKey first, RedisKey
public bool IsConnected(RedisKey key, CommandFlags flags = CommandFlags.None) =>
Inner.IsConnected(ToInner(key), flags);

public Task<bool> KeyCopyAsync(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None) =>
Inner.KeyCopyAsync(ToInner(sourceKey), ToInner(destinationKey), destinationDatabase, replace, flags);

public Task<long> KeyDeleteAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) =>
Inner.KeyDeleteAsync(ToInner(keys), flags);

Expand Down
2 changes: 2 additions & 0 deletions src/StackExchange.Redis/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,7 @@ StackExchange.Redis.IDatabase.HyperLogLogLength(StackExchange.Redis.RedisKey[]!
StackExchange.Redis.IDatabase.HyperLogLogMerge(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void
StackExchange.Redis.IDatabase.HyperLogLogMerge(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! sourceKeys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void
StackExchange.Redis.IDatabase.IdentifyEndpoint(StackExchange.Redis.RedisKey key = default(StackExchange.Redis.RedisKey), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Net.EndPoint?
StackExchange.Redis.IDatabase.KeyCopy(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool
StackExchange.Redis.IDatabase.KeyDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool
StackExchange.Redis.IDatabase.KeyDelete(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long
StackExchange.Redis.IDatabase.KeyDump(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> byte[]?
Expand Down Expand Up @@ -703,6 +704,7 @@ StackExchange.Redis.IDatabaseAsync.HyperLogLogMergeAsync(StackExchange.Redis.Red
StackExchange.Redis.IDatabaseAsync.HyperLogLogMergeAsync(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! sourceKeys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
StackExchange.Redis.IDatabaseAsync.IdentifyEndpointAsync(StackExchange.Redis.RedisKey key = default(StackExchange.Redis.RedisKey), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<System.Net.EndPoint?>!
StackExchange.Redis.IDatabaseAsync.IsConnected(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool
StackExchange.Redis.IDatabaseAsync.KeyCopyAsync(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<bool>!
StackExchange.Redis.IDatabaseAsync.KeyDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<bool>!
StackExchange.Redis.IDatabaseAsync.KeyDeleteAsync(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<long>!
StackExchange.Redis.IDatabaseAsync.KeyDumpAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<byte[]?>!
Expand Down
22 changes: 22 additions & 0 deletions src/StackExchange.Redis/RedisDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,18 @@ public bool IsConnected(RedisKey key, CommandFlags flags = CommandFlags.None)
return server?.IsConnected == true;
}

public bool KeyCopy(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None)
{
var msg = GetCopyMessage(sourceKey, destinationKey, destinationDatabase, replace, flags);
return ExecuteSync(msg, ResultProcessor.Boolean);
}

public Task<bool> KeyCopyAsync(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None)
{
var msg = GetCopyMessage(sourceKey, destinationKey, destinationDatabase, replace, flags);
return ExecuteAsync(msg, ResultProcessor.Boolean);
}

public bool KeyDelete(RedisKey key, CommandFlags flags = CommandFlags.None)
{
var cmd = GetDeleteCommand(key, flags, out var server);
Expand Down Expand Up @@ -2733,6 +2745,16 @@ public Task<RedisValue> StringSetRangeAsync(RedisKey key, long offset, RedisValu
_ => throw new ArgumentException("Expiry time must be either Utc or Local", nameof(when)),
};

private Message GetCopyMessage(in RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase, bool replace, CommandFlags flags) =>
destinationDatabase switch
{
< -1 => throw new ArgumentOutOfRangeException(nameof(destinationDatabase)),
-1 when replace => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey, RedisLiterals.REPLACE),
-1 => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey),
_ when replace => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey, RedisLiterals.DB, destinationDatabase, RedisLiterals.REPLACE),
_ => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey, RedisLiterals.DB, destinationDatabase),
};

private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, TimeSpan? expiry, out ServerEndPoint? server)
{
TimeSpan duration;
Expand Down
5 changes: 0 additions & 5 deletions src/StackExchange.Redis/RedisFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,6 @@ public RedisFeatures(Version version)
/// </summary>
public bool GetDelete => Version >= v6_2_0;

/// <summary>
/// Does GETEX exist?
/// </summary>
internal bool GetEx => Version >= v6_2_0;

/// <summary>
/// Is HSTRLEN available?
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/StackExchange.Redis/RedisLiterals.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Text;

namespace StackExchange.Redis
{
Expand Down Expand Up @@ -54,6 +53,7 @@ public static readonly RedisValue
CHANNELS = "CHANNELS",
COPY = "COPY",
COUNT = "COUNT",
DB = "DB",
DESC = "DESC",
DOCTOR = "DOCTOR",
EX = "EX",
Expand Down
73 changes: 73 additions & 0 deletions tests/StackExchange.Redis.Tests/Copy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;

namespace StackExchange.Redis.Tests
{
[Collection(SharedConnectionFixture.Key)]
public class Copy : TestBase
{
public Copy(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { }

[Fact]
public async Task Basic()
{
using var muxer = Create();
Skip.IfBelow(muxer, RedisFeatures.v6_2_0);

var db = muxer.GetDatabase();
var src = Me();
var dest = Me() + "2";
_ = db.KeyDelete(dest);

_ = db.StringSetAsync(src, "Heyyyyy");
var ke1 = db.KeyCopyAsync(src, dest).ForAwait();
var ku1 = db.StringGet(dest);
Assert.True(await ke1);
Assert.True(ku1.Equals("Heyyyyy"));
}

[Fact]
public async Task CrossDB()
{
using var muxer = Create();
Skip.IfBelow(muxer, RedisFeatures.v6_2_0);

var db = muxer.GetDatabase();
var dbDestId = TestConfig.GetDedicatedDB(muxer);
var dbDest = muxer.GetDatabase(dbDestId);

var src = Me();
var dest = Me() + "2";
dbDest.KeyDelete(dest);

_ = db.StringSetAsync(src, "Heyyyyy");
var ke1 = db.KeyCopyAsync(src, dest, dbDestId).ForAwait();
var ku1 = dbDest.StringGet(dest);
Assert.True(await ke1);
Assert.True(ku1.Equals("Heyyyyy"));

await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => db.KeyCopyAsync(src, dest, destinationDatabase: -10));
}

[Fact]
public async Task WithReplace()
{
using var muxer = Create();
Skip.IfBelow(muxer, RedisFeatures.v6_2_0);

var db = muxer.GetDatabase();
var src = Me();
var dest = Me() + "2";
_ = db.StringSetAsync(src, "foo1");
_ = db.StringSetAsync(dest, "foo2");
var ke1 = db.KeyCopyAsync(src, dest).ForAwait();
var ke2 = db.KeyCopyAsync(src, dest, replace: true).ForAwait();
var ku1 = db.StringGet(dest);
Assert.False(await ke1); // Should fail when not using replace and destination key exist
Assert.True(await ke2);
Assert.True(ku1.Equals("foo1"));
}
}
}
7 changes: 7 additions & 0 deletions tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,13 @@ public void IdentifyEndpoint()
mock.Verify(_ => _.IdentifyEndpoint("prefix:key", CommandFlags.None));
}

[Fact]
public void KeyCopy()
{
wrapper.KeyCopy("key", "destination", flags: CommandFlags.None);
mock.Verify(_ => _.KeyCopy("prefix:key", "prefix:destination", -1, false, CommandFlags.None));
}

[Fact]
public void KeyDelete_1()
{
Expand Down
12 changes: 12 additions & 0 deletions tests/StackExchange.Redis.Tests/Helpers/Skip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ public static void IfNoConfig(string prop, [NotNull] List<string>? values)
}
}

public static void IfBelow(IConnectionMultiplexer conn, Version minVersion)
{
var serverVersion = conn.GetServer(conn.GetEndPoints()[0]).Version;
if (minVersion > serverVersion)
{
throw new SkipTestException($"Requires server version {minVersion}, but server is only {serverVersion}.")
{
MissingFeatures = $"Server version >= {minVersion}."
};
}
}

public static void IfMissingFeature(IConnectionMultiplexer conn, string feature, Func<RedisFeatures, bool> check)
{
var features = conn.GetServer(conn.GetEndPoints()[0]).Features;
Expand Down
9 changes: 5 additions & 4 deletions tests/StackExchange.Redis.Tests/Strings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public async Task Set()
public async Task StringGetSetExpiryNoValue()
{
using var muxer = Create();
Skip.IfMissingFeature(muxer, nameof(RedisFeatures.GetEx), r => r.GetEx);
Skip.IfBelow(muxer, RedisFeatures.v6_2_0);

var conn = muxer.GetDatabase();
var key = Me();
Expand All @@ -87,7 +87,7 @@ public async Task StringGetSetExpiryNoValue()
public async Task StringGetSetExpiryRelative()
{
using var muxer = Create();
Skip.IfMissingFeature(muxer, nameof(RedisFeatures.GetEx), r => r.GetEx);
Skip.IfBelow(muxer, RedisFeatures.v6_2_0);

var conn = muxer.GetDatabase();
var key = Me();
Expand All @@ -107,7 +107,8 @@ public async Task StringGetSetExpiryRelative()
public async Task StringGetSetExpiryAbsolute()
{
using var muxer = Create();
Skip.IfMissingFeature(muxer, nameof(RedisFeatures.GetEx), r => r.GetEx);
Skip.IfBelow(muxer, RedisFeatures.v6_2_0);

var conn = muxer.GetDatabase();
var key = Me();
conn.KeyDelete(key, CommandFlags.FireAndForget);
Expand All @@ -131,7 +132,7 @@ public async Task StringGetSetExpiryAbsolute()
public async Task StringGetSetExpiryPersist()
{
using var muxer = Create();
Skip.IfMissingFeature(muxer, nameof(RedisFeatures.GetEx), r => r.GetEx);
Skip.IfBelow(muxer, RedisFeatures.v6_2_0);

var conn = muxer.GetDatabase();
var key = Me();
Expand Down
7 changes: 7 additions & 0 deletions tests/StackExchange.Redis.Tests/WrapperBaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,13 @@ public void IsConnected()
mock.Verify(_ => _.IsConnected("prefix:key", CommandFlags.None));
}

[Fact]
public void KeyCopyAsync()
{
wrapper.KeyCopyAsync("key", "destination", flags: CommandFlags.None);
mock.Verify(_ => _.KeyCopyAsync("prefix:key", "prefix:destination", -1, false, CommandFlags.None));
}

[Fact]
public void KeyDeleteAsync_1()
{
Expand Down