diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 7572f458..914ea9c4 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -39,10 +39,10 @@ jobs: fail-fast: false matrix: # Full version set runs only on the nightly schedule; PRs and pushes - # test a reduced set (7.2 LTS, 7.4 LTS, 8.2 LTS, 8.8, 8.10 current stable) to keep CI fast. + # test a reduced set (7.2 LTS, 7.4 LTS, 8.2 LTS, 8.8, 8.10 current stable, 8.12 preview) to keep CI fast. redis-version: ${{ fromJSON(github.event_name == 'schedule' - && '["8.10", "8.8", "8.6", "8.4", "8.2", "7.4", "7.2", "6.2"]' - || '["8.10", "8.8", "8.2", "7.4", "7.2"]') }} + && '["8.12", "8.10", "8.8", "8.6", "8.4", "8.2", "7.4", "7.2", "6.2"]' + || '["8.12", "8.10", "8.8", "8.2", "7.4", "7.2"]') }} dotnet-version: ['8.0', '9.0', '10.0'] name: Redis ${{ matrix.redis-version }}; .NET ${{ matrix.dotnet-version }}; steps: diff --git a/src/NRedisStack/CountMinSketch/CmsCommandBuilder.cs b/src/NRedisStack/CountMinSketch/CmsCommandBuilder.cs index 2f9dd6b2..1676018c 100644 --- a/src/NRedisStack/CountMinSketch/CmsCommandBuilder.cs +++ b/src/NRedisStack/CountMinSketch/CmsCommandBuilder.cs @@ -37,11 +37,36 @@ public static SerializedCommand InitByDim(RedisKey key, long width, long depth) return new(CommandCategories.WriteAccumulating, CMS.INITBYDIM, key, width, depth); } + public static SerializedCommand InitByDim(RedisKey key, long width, long depth, int cellSize) + { + List args = [key, width, depth]; + AddCellSize(args, cellSize); + + return new(CommandCategories.WriteAccumulating, CMS.INITBYDIM, args); + } + public static SerializedCommand InitByProb(RedisKey key, double error, double probability) { return new(CommandCategories.WriteAccumulating, CMS.INITBYPROB, key, error, probability); } + public static SerializedCommand InitByProb(RedisKey key, double error, double probability, int cellSize) + { + List args = [key, error, probability]; + AddCellSize(args, cellSize); + + return new(CommandCategories.WriteAccumulating, CMS.INITBYPROB, args); + } + + private static void AddCellSize(List args, int cellSize) + { + if (cellSize is not (1 or 2 or 4 or 8)) + throw new ArgumentOutOfRangeException(nameof(cellSize), cellSize, "Cell size must be 1, 2, 4, or 8."); + + args.Add(CmsArgs.CELL_SIZE); + args.Add(cellSize); + } + public static SerializedCommand Merge(RedisValue destination, long numKeys, RedisValue[] source, long[]? weight = null) { diff --git a/src/NRedisStack/CountMinSketch/CmsCommands.cs b/src/NRedisStack/CountMinSketch/CmsCommands.cs index a193cd0b..0d0bf8e1 100644 --- a/src/NRedisStack/CountMinSketch/CmsCommands.cs +++ b/src/NRedisStack/CountMinSketch/CmsCommands.cs @@ -37,12 +37,24 @@ public bool InitByDim(RedisKey key, long width, long depth) return _db.Execute(CmsCommandBuilder.InitByDim(key, width, depth)).OKtoBoolean(); } + /// + public bool InitByDim(RedisKey key, long width, long depth, int cellSize) + { + return _db.Execute(CmsCommandBuilder.InitByDim(key, width, depth, cellSize)).OKtoBoolean(); + } + /// public bool InitByProb(RedisKey key, double error, double probability) { return _db.Execute(CmsCommandBuilder.InitByProb(key, error, probability)).OKtoBoolean(); } + /// + public bool InitByProb(RedisKey key, double error, double probability, int cellSize) + { + return _db.Execute(CmsCommandBuilder.InitByProb(key, error, probability, cellSize)).OKtoBoolean(); + } + /// public bool Merge(RedisValue destination, long numKeys, RedisValue[] source, long[]? weight = null) { diff --git a/src/NRedisStack/CountMinSketch/CmsCommandsAsync.cs b/src/NRedisStack/CountMinSketch/CmsCommandsAsync.cs index 7927a153..1a7b6dbe 100644 --- a/src/NRedisStack/CountMinSketch/CmsCommandsAsync.cs +++ b/src/NRedisStack/CountMinSketch/CmsCommandsAsync.cs @@ -37,12 +37,24 @@ public async Task InitByDimAsync(RedisKey key, long width, long depth) return (await _db.ExecuteAsync(CmsCommandBuilder.InitByDim(key, width, depth))).OKtoBoolean(); } + /// + public async Task InitByDimAsync(RedisKey key, long width, long depth, int cellSize) + { + return (await _db.ExecuteAsync(CmsCommandBuilder.InitByDim(key, width, depth, cellSize))).OKtoBoolean(); + } + /// public async Task InitByProbAsync(RedisKey key, double error, double probability) { return (await _db.ExecuteAsync(CmsCommandBuilder.InitByProb(key, error, probability))).OKtoBoolean(); } + /// + public async Task InitByProbAsync(RedisKey key, double error, double probability, int cellSize) + { + return (await _db.ExecuteAsync(CmsCommandBuilder.InitByProb(key, error, probability, cellSize))).OKtoBoolean(); + } + /// public async Task MergeAsync(RedisValue destination, long numKeys, RedisValue[] source, long[]? weight = null) { diff --git a/src/NRedisStack/CountMinSketch/ICmsCommands.cs b/src/NRedisStack/CountMinSketch/ICmsCommands.cs index 178d9cfa..21b2c05d 100644 --- a/src/NRedisStack/CountMinSketch/ICmsCommands.cs +++ b/src/NRedisStack/CountMinSketch/ICmsCommands.cs @@ -44,6 +44,18 @@ public interface ICmsCommands /// bool InitByDim(RedisKey key, long width, long depth); + /// + /// Initializes a Count-Min Sketch to dimensions specified by user. + /// + /// TThe name of the sketch. + /// Number of counters in each array. Reduces the error size. + /// Number of counter-arrays. Reduces the probability for an error + /// of a certain size (percentage of total count). + /// The number of bytes per cell (1, 2, 4, or 8). + /// if if executed correctly, Error otherwise. + /// + bool InitByDim(RedisKey key, long width, long depth, int cellSize); + /// /// Initializes a Count-Min Sketch to accommodate requested tolerances. /// @@ -54,6 +66,17 @@ public interface ICmsCommands /// bool InitByProb(RedisKey key, double error, double probability); + /// + /// Initializes a Count-Min Sketch to accommodate requested tolerances. + /// + /// The name of the sketch. + /// Estimate size of error. + /// The desired probability for inflated count. + /// The number of bytes per cell (1, 2, 4, or 8). + /// if if executed correctly, Error otherwise. + /// + bool InitByProb(RedisKey key, double error, double probability, int cellSize); + /// /// Merges several sketches into one sketch. /// diff --git a/src/NRedisStack/CountMinSketch/ICmsCommandsAsync.cs b/src/NRedisStack/CountMinSketch/ICmsCommandsAsync.cs index 1562d70f..8db46d54 100644 --- a/src/NRedisStack/CountMinSketch/ICmsCommandsAsync.cs +++ b/src/NRedisStack/CountMinSketch/ICmsCommandsAsync.cs @@ -44,6 +44,18 @@ public interface ICmsCommandsAsync /// Task InitByDimAsync(RedisKey key, long width, long depth); + /// + /// Initializes a Count-Min Sketch to dimensions specified by user. + /// + /// TThe name of the sketch. + /// Number of counters in each array. Reduces the error size. + /// Number of counter-arrays. Reduces the probability for an error + /// of a certain size (percentage of total count). + /// The number of bytes per cell (1, 2, 4, or 8). + /// if if executed correctly, Error otherwise. + /// + Task InitByDimAsync(RedisKey key, long width, long depth, int cellSize); + /// /// Initializes a Count-Min Sketch to accommodate requested tolerances. /// @@ -54,6 +66,17 @@ public interface ICmsCommandsAsync /// Task InitByProbAsync(RedisKey key, double error, double probability); + /// + /// Initializes a Count-Min Sketch to accommodate requested tolerances. + /// + /// The name of the sketch. + /// Estimate size of error. + /// The desired probability for inflated count. + /// The number of bytes per cell (1, 2, 4, or 8). + /// if if executed correctly, Error otherwise. + /// + Task InitByProbAsync(RedisKey key, double error, double probability, int cellSize); + /// /// Merges several sketches into one sketch. /// diff --git a/src/NRedisStack/CountMinSketch/Literals/CommandArgs.cs b/src/NRedisStack/CountMinSketch/Literals/CommandArgs.cs index 9f9817eb..001d0c3f 100644 --- a/src/NRedisStack/CountMinSketch/Literals/CommandArgs.cs +++ b/src/NRedisStack/CountMinSketch/Literals/CommandArgs.cs @@ -3,4 +3,5 @@ namespace NRedisStack.CountMinSketch.Literals; internal class CmsArgs { public const string WEIGHTS = "WEIGHTS"; + public const string CELL_SIZE = "CELL_SIZE"; } \ No newline at end of file diff --git a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt index 666bcf99..9a7151b7 100644 --- a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt @@ -1,2 +1,12 @@ #nullable enable NRedisStack.CountMinSketch.DataTypes.CmsInformation.CellSize.get -> int +NRedisStack.CmsCommands.InitByDim(StackExchange.Redis.RedisKey key, long width, long depth, int cellSize) -> bool +NRedisStack.CmsCommands.InitByProb(StackExchange.Redis.RedisKey key, double error, double probability, int cellSize) -> bool +NRedisStack.CmsCommandsAsync.InitByDimAsync(StackExchange.Redis.RedisKey key, long width, long depth, int cellSize) -> System.Threading.Tasks.Task! +NRedisStack.CmsCommandsAsync.InitByProbAsync(StackExchange.Redis.RedisKey key, double error, double probability, int cellSize) -> System.Threading.Tasks.Task! +NRedisStack.ICmsCommands.InitByDim(StackExchange.Redis.RedisKey key, long width, long depth, int cellSize) -> bool +NRedisStack.ICmsCommands.InitByProb(StackExchange.Redis.RedisKey key, double error, double probability, int cellSize) -> bool +NRedisStack.ICmsCommandsAsync.InitByDimAsync(StackExchange.Redis.RedisKey key, long width, long depth, int cellSize) -> System.Threading.Tasks.Task! +NRedisStack.ICmsCommandsAsync.InitByProbAsync(StackExchange.Redis.RedisKey key, double error, double probability, int cellSize) -> System.Threading.Tasks.Task! +static NRedisStack.CmsCommandBuilder.InitByDim(StackExchange.Redis.RedisKey key, long width, long depth, int cellSize) -> NRedisStack.RedisStackCommands.SerializedCommand! +static NRedisStack.CmsCommandBuilder.InitByProb(StackExchange.Redis.RedisKey key, double error, double probability, int cellSize) -> NRedisStack.RedisStackCommands.SerializedCommand! diff --git a/src/NRedisStack/ResponseParser.cs b/src/NRedisStack/ResponseParser.cs index 1fd1024a..b75115c9 100644 --- a/src/NRedisStack/ResponseParser.cs +++ b/src/NRedisStack/ResponseParser.cs @@ -516,7 +516,7 @@ public static CmsInformation case "count": count = (long)redisResults[i]; break; - case "cell size": + case "cell_size": cellSize = (int)redisResults[i]; break; } diff --git a/tests/NRedisStack.Tests/CountMinSketch/CmsTests.cs b/tests/NRedisStack.Tests/CountMinSketch/CmsTests.cs index f86491df..5d213508 100644 --- a/tests/NRedisStack.Tests/CountMinSketch/CmsTests.cs +++ b/tests/NRedisStack.Tests/CountMinSketch/CmsTests.cs @@ -39,6 +39,48 @@ public async Task TestInitByDimAsync(string endpointId) Assert.Equal(0, info.Count); } + [SkipIfRedisTheory(Comparison.LessThan, "8.12.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestInitByDimCellSize(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var cms = db.CMS(); + + cms.InitByDim(key, 16, 4, cellSize: 1); + var info = cms.Info(key); + + Assert.Equal(16, info.Width); + Assert.Equal(4, info.Depth); + Assert.Equal(0, info.Count); + Assert.Equal(1, info.CellSize); + } + + [SkipIfRedisTheory(Comparison.LessThan, "8.12.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestInitByDimCellSizeAsync(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var cms = db.CMS(); + + await cms.InitByDimAsync(key, 16, 4, cellSize: 8); + var info = await cms.InfoAsync(key); + + Assert.Equal(16, info.Width); + Assert.Equal(4, info.Depth); + Assert.Equal(0, info.Count); + Assert.Equal(8, info.CellSize); + } + + [Theory] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestInitByDimInvalidCellSize(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var cms = db.CMS(); + + Assert.Throws(() => cms.InitByDim(key, 16, 4, cellSize: 3)); + } + [Theory] [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestInitByProb(string endpointId) @@ -54,6 +96,22 @@ public void TestInitByProb(string endpointId) Assert.Equal(0, info.Count); } + [SkipIfRedisTheory(Comparison.LessThan, "8.12.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestInitByProbCellSize(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var cms = db.CMS(); + + cms.InitByProb(key, 0.01, 0.01, cellSize: 2); + var info = cms.Info(key); + + Assert.Equal(200, info.Width); + Assert.Equal(7, info.Depth); + Assert.Equal(0, info.Count); + Assert.Equal(2, info.CellSize); + } + [Theory] [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public async Task TestInitByProbAsync(string endpointId) @@ -127,6 +185,38 @@ public async Task TestIncrByAsync(string endpointId) } + [SkipIfRedisTheory(Comparison.LessThan, "8.12.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public void TestIncrByNegative(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var cms = db.CMS(); + + cms.InitByDim(key, 1000, 5); + cms.IncrBy(key, "foo", 10); + var resp = cms.IncrBy(key, "foo", -4); + Assert.Equal(6, resp); + + var info = cms.Info(key); + Assert.Equal(6, info.Count); + } + + [SkipIfRedisTheory(Comparison.LessThan, "8.12.0")] + [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestIncrByNegativeAsync(string endpointId) + { + var db = GetCleanDatabase(endpointId); + var cms = db.CMS(); + + await cms.InitByDimAsync(key, 1000, 5); + await cms.IncrByAsync(key, "foo", 10); + var resp = await cms.IncrByAsync(key, "foo", -4); + Assert.Equal(6, resp); + + var info = await cms.InfoAsync(key); + Assert.Equal(6, info.Count); + } + [Theory] [MemberData(nameof(EndpointsFixture.Env.AllEnvironments), MemberType = typeof(EndpointsFixture.Env))] public void TestIncrByMultipleArgs(string endpointId) diff --git a/tests/dockers/.env.v8.12 b/tests/dockers/.env.v8.12 new file mode 100644 index 00000000..55ecf15f --- /dev/null +++ b/tests/dockers/.env.v8.12 @@ -0,0 +1,7 @@ +# Environment variables for Redis 8.12 +# Used by the run-tests GitHub Action + +# No official 8.12 release image exists yet, so this is pinned to an unstable +# preview build. Unlike the other .env.v* files, this tag is not guaranteed to +# stay available - replace with the real release image once 8.12 ships. +CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:unstable-34786335206-debian