From b9f3999a30e2ab3411612fa6ee02fed7ebdb5671 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Wed, 15 Oct 2025 10:04:27 -0700 Subject: [PATCH 01/13] Add bitadd Signed-off-by: Alex Rehnby-Martin --- .../Valkey.Glide/BaseClient.BitmapCommands.cs | 16 +++++ .../Valkey.Glide/Commands/IBitmapCommands.cs | 30 ++++++++++ .../Internals/Request.BitmapCommands.cs | 11 ++++ .../Pipeline/BaseBatch.BitmapCommands.cs | 13 +++++ sources/Valkey.Glide/Pipeline/IBatch.cs | 2 +- .../Pipeline/IBatchBitmapCommands.cs | 13 +++++ .../BitmapCommandTests.cs | 58 +++++++++++++++++++ 7 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 sources/Valkey.Glide/BaseClient.BitmapCommands.cs create mode 100644 sources/Valkey.Glide/Commands/IBitmapCommands.cs create mode 100644 sources/Valkey.Glide/Internals/Request.BitmapCommands.cs create mode 100644 sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs create mode 100644 sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs create mode 100644 tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs diff --git a/sources/Valkey.Glide/BaseClient.BitmapCommands.cs b/sources/Valkey.Glide/BaseClient.BitmapCommands.cs new file mode 100644 index 00000000..c2cbbd25 --- /dev/null +++ b/sources/Valkey.Glide/BaseClient.BitmapCommands.cs @@ -0,0 +1,16 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +using Valkey.Glide.Commands; +using Valkey.Glide.Internals; + +namespace Valkey.Glide; + +public abstract partial class BaseClient : IBitmapCommands +{ + /// + public async Task StringGetBitAsync(ValkeyKey key, long offset, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.GetBitAsync(key, offset)); + } +} \ No newline at end of file diff --git a/sources/Valkey.Glide/Commands/IBitmapCommands.cs b/sources/Valkey.Glide/Commands/IBitmapCommands.cs new file mode 100644 index 00000000..609077f2 --- /dev/null +++ b/sources/Valkey.Glide/Commands/IBitmapCommands.cs @@ -0,0 +1,30 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +namespace Valkey.Glide.Commands; + +/// +/// Supports commands for the "Bitmap Commands" group for standalone and cluster clients. +///
+/// See more on valkey.io. +///
+public interface IBitmapCommands +{ + /// + /// Returns the bit value at offset in the string value stored at key. + /// + /// + /// The key of the string. + /// The offset in the string to get the bit at. + /// The flags to use for this operation. Currently flags are ignored. + /// The bit value stored at offset. Returns 0 if the key does not exist or if the offset is beyond the string length. + /// + /// + /// + /// await client.StringSetAsync("mykey", "A"); // ASCII 'A' is 01000001 + /// bool bit = await client.StringGetBitAsync("mykey", 1); + /// Console.WriteLine(bit); // Output: true (bit 1 is set) + /// + /// + /// + Task StringGetBitAsync(ValkeyKey key, long offset, CommandFlags flags = CommandFlags.None); +} \ No newline at end of file diff --git a/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs b/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs new file mode 100644 index 00000000..a2592e22 --- /dev/null +++ b/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs @@ -0,0 +1,11 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +using static Valkey.Glide.Internals.FFI; + +namespace Valkey.Glide.Internals; + +internal partial class Request +{ + public static Cmd GetBitAsync(ValkeyKey key, long offset) + => new(RequestType.GetBit, [key.ToGlideString(), offset.ToGlideString()], false, response => (long)response != 0); +} \ No newline at end of file diff --git a/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs b/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs new file mode 100644 index 00000000..c1a6f224 --- /dev/null +++ b/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs @@ -0,0 +1,13 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +using Valkey.Glide.Internals; + +namespace Valkey.Glide.Pipeline; + +public abstract partial class BaseBatch where T : BaseBatch +{ + /// + public T StringGetBitAsync(ValkeyKey key, long offset) => AddCmd(Request.GetBitAsync(key, offset)); + + IBatch IBatchBitmapCommands.StringGetBit(ValkeyKey key, long offset) => StringGetBitAsync(key, offset); +} \ No newline at end of file diff --git a/sources/Valkey.Glide/Pipeline/IBatch.cs b/sources/Valkey.Glide/Pipeline/IBatch.cs index 52470d61..4fd2d714 100644 --- a/sources/Valkey.Glide/Pipeline/IBatch.cs +++ b/sources/Valkey.Glide/Pipeline/IBatch.cs @@ -7,7 +7,7 @@ namespace Valkey.Glide.Pipeline; // BaseBatch was split into two types, one for docs, another for the impl. This also ease the testing. -internal interface IBatch : IBatchSetCommands, IBatchStringCommands, IBatchListCommands, IBatchSortedSetCommands, IBatchGenericCommands, IBatchConnectionManagementCommands, IBatchHashCommands, IBatchServerManagementCommands, IBatchHyperLogLogCommands +internal interface IBatch : IBatchSetCommands, IBatchStringCommands, IBatchListCommands, IBatchSortedSetCommands, IBatchGenericCommands, IBatchConnectionManagementCommands, IBatchHashCommands, IBatchServerManagementCommands, IBatchHyperLogLogCommands, IBatchBitmapCommands { // inherit all docs except `remarks` section which stores en example (not relevant for batch) // and returns section, because we customize it. diff --git a/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs b/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs new file mode 100644 index 00000000..a1b0f66c --- /dev/null +++ b/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs @@ -0,0 +1,13 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +namespace Valkey.Glide.Pipeline; + +/// +/// Supports commands for the "Bitmap Commands" group for batch requests. +/// +internal interface IBatchBitmapCommands +{ + /// + /// Command Response - + IBatch StringGetBit(ValkeyKey key, long offset); +} \ No newline at end of file diff --git a/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs new file mode 100644 index 00000000..347c4fa3 --- /dev/null +++ b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs @@ -0,0 +1,58 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +namespace Valkey.Glide.IntegrationTests; + +public class BitmapCommandTests(TestConfiguration config) +{ + public TestConfiguration Config { get; } = config; + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GetBit_ReturnsCorrectBitValue(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Set a string value - ASCII 'A' is 01000001 in binary + await client.StringSetAsync(key, "A"); + + // Test bit positions in 'A' (01000001) + bool bit0 = await client.StringGetBitAsync(key, 0); // Should be false (0) + bool bit1 = await client.StringGetBitAsync(key, 1); // Should be true (1) + bool bit2 = await client.StringGetBitAsync(key, 2); // Should be false (0) + bool bit6 = await client.StringGetBitAsync(key, 6); // Should be false (0) + bool bit7 = await client.StringGetBitAsync(key, 7); // Should be true (1) + + Assert.False(bit0); + Assert.True(bit1); + Assert.False(bit2); + Assert.False(bit6); + Assert.True(bit7); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GetBit_NonExistentKey_ReturnsFalse(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Test bit on non-existent key + bool bit = await client.StringGetBitAsync(key, 0); + + Assert.False(bit); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GetBit_OffsetBeyondString_ReturnsFalse(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Set a short string + await client.StringSetAsync(key, "A"); + + // Test bit beyond the string length + bool bit = await client.StringGetBitAsync(key, 100); + + Assert.False(bit); + } +} \ No newline at end of file From 9b153f1361c02e2cf6cdf45c450d1d8d81407414 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Wed, 15 Oct 2025 10:51:53 -0700 Subject: [PATCH 02/13] Add setbit Signed-off-by: Alex Rehnby-Martin --- .../Valkey.Glide/BaseClient.BitmapCommands.cs | 7 ++ .../Valkey.Glide/Commands/IBitmapCommands.cs | 19 +++++ .../Internals/Request.BitmapCommands.cs | 3 + .../Pipeline/BaseBatch.BitmapCommands.cs | 4 + .../Pipeline/IBatchBitmapCommands.cs | 4 + .../BitmapCommandTests.cs | 75 +++++++++++++++++++ 6 files changed, 112 insertions(+) diff --git a/sources/Valkey.Glide/BaseClient.BitmapCommands.cs b/sources/Valkey.Glide/BaseClient.BitmapCommands.cs index c2cbbd25..3d43ef41 100644 --- a/sources/Valkey.Glide/BaseClient.BitmapCommands.cs +++ b/sources/Valkey.Glide/BaseClient.BitmapCommands.cs @@ -13,4 +13,11 @@ public async Task StringGetBitAsync(ValkeyKey key, long offset, CommandFla Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); return await Command(Request.GetBitAsync(key, offset)); } + + /// + public async Task StringSetBitAsync(ValkeyKey key, long offset, bool value, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.SetBitAsync(key, offset, value)); + } } \ No newline at end of file diff --git a/sources/Valkey.Glide/Commands/IBitmapCommands.cs b/sources/Valkey.Glide/Commands/IBitmapCommands.cs index 609077f2..9901cc78 100644 --- a/sources/Valkey.Glide/Commands/IBitmapCommands.cs +++ b/sources/Valkey.Glide/Commands/IBitmapCommands.cs @@ -27,4 +27,23 @@ public interface IBitmapCommands /// /// Task StringGetBitAsync(ValkeyKey key, long offset, CommandFlags flags = CommandFlags.None); + + /// + /// Sets or clears the bit at offset in the string value stored at key. + /// + /// + /// The key of the string. + /// The offset in the string to set the bit at. + /// The bit value to set (true for 1, false for 0). + /// The flags to use for this operation. Currently flags are ignored. + /// The original bit value stored at offset. + /// + /// + /// + /// bool oldBit = await client.StringSetBitAsync("mykey", 1, true); + /// Console.WriteLine(oldBit); // Output: false (original bit value) + /// + /// + /// + Task StringSetBitAsync(ValkeyKey key, long offset, bool value, CommandFlags flags = CommandFlags.None); } \ No newline at end of file diff --git a/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs b/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs index a2592e22..4ccb1c2e 100644 --- a/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs @@ -8,4 +8,7 @@ internal partial class Request { public static Cmd GetBitAsync(ValkeyKey key, long offset) => new(RequestType.GetBit, [key.ToGlideString(), offset.ToGlideString()], false, response => (long)response != 0); + + public static Cmd SetBitAsync(ValkeyKey key, long offset, bool value) + => new(RequestType.SetBit, [key.ToGlideString(), offset.ToGlideString(), (value ? 1 : 0).ToGlideString()], false, response => (long)response != 0); } \ No newline at end of file diff --git a/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs b/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs index c1a6f224..6acaf403 100644 --- a/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs +++ b/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs @@ -9,5 +9,9 @@ public abstract partial class BaseBatch where T : BaseBatch /// public T StringGetBitAsync(ValkeyKey key, long offset) => AddCmd(Request.GetBitAsync(key, offset)); + /// + public T StringSetBitAsync(ValkeyKey key, long offset, bool value) => AddCmd(Request.SetBitAsync(key, offset, value)); + IBatch IBatchBitmapCommands.StringGetBit(ValkeyKey key, long offset) => StringGetBitAsync(key, offset); + IBatch IBatchBitmapCommands.StringSetBit(ValkeyKey key, long offset, bool value) => StringSetBitAsync(key, offset, value); } \ No newline at end of file diff --git a/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs b/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs index a1b0f66c..e8efed03 100644 --- a/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs +++ b/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs @@ -10,4 +10,8 @@ internal interface IBatchBitmapCommands /// /// Command Response - IBatch StringGetBit(ValkeyKey key, long offset); + + /// + /// Command Response - + IBatch StringSetBit(ValkeyKey key, long offset, bool value); } \ No newline at end of file diff --git a/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs index 347c4fa3..80adf6cc 100644 --- a/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs @@ -55,4 +55,79 @@ public async Task GetBit_OffsetBeyondString_ReturnsFalse(BaseClient client) Assert.False(bit); } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task SetBit_SetsAndReturnsOriginalValue(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Set bit 1 to true (original should be false) + bool originalBit = await client.StringSetBitAsync(key, 1, true); + Assert.False(originalBit); + + // Verify bit is now set + bool currentBit = await client.StringGetBitAsync(key, 1); + Assert.True(currentBit); + + // Set bit 1 to false (original should be true) + bool originalBit2 = await client.StringSetBitAsync(key, 1, false); + Assert.True(originalBit2); + + // Verify bit is now cleared + bool currentBit2 = await client.StringGetBitAsync(key, 1); + Assert.False(currentBit2); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task SetBit_NonExistentKey_CreatesKey(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Set bit on non-existent key + bool originalBit = await client.StringSetBitAsync(key, 0, true); + Assert.False(originalBit); + + // Verify key was created and bit is set + bool currentBit = await client.StringGetBitAsync(key, 0); + Assert.True(currentBit); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GetSetBit_CombinedOperations_WorksTogether(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Initially all bits should be 0 + bool bit0 = await client.StringGetBitAsync(key, 0); + bool bit1 = await client.StringGetBitAsync(key, 1); + Assert.False(bit0); + Assert.False(bit1); + + // Set bit 0 to 1 + bool originalBit0 = await client.StringSetBitAsync(key, 0, true); + Assert.False(originalBit0); // Was 0 + + // Set bit 1 to 1 + bool originalBit1 = await client.StringSetBitAsync(key, 1, true); + Assert.False(originalBit1); // Was 0 + + // Verify both bits are now set + bool newBit0 = await client.StringGetBitAsync(key, 0); + bool newBit1 = await client.StringGetBitAsync(key, 1); + Assert.True(newBit0); + Assert.True(newBit1); + + // Clear bit 0 + bool clearBit0 = await client.StringSetBitAsync(key, 0, false); + Assert.True(clearBit0); // Was 1 + + // Verify bit 0 is cleared but bit 1 remains set + bool finalBit0 = await client.StringGetBitAsync(key, 0); + bool finalBit1 = await client.StringGetBitAsync(key, 1); + Assert.False(finalBit0); + Assert.True(finalBit1); + } } \ No newline at end of file From 1eb3450e9e1e1ca2ec02a5235f06d5d045875098 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Wed, 15 Oct 2025 11:24:23 -0700 Subject: [PATCH 03/13] bitcount Signed-off-by: Alex Rehnby-Martin --- .../Valkey.Glide/BaseClient.BitmapCommands.cs | 7 +++ .../Valkey.Glide/Commands/IBitmapCommands.cs | 21 ++++++++ .../Internals/Request.BitmapCommands.cs | 10 ++++ .../Pipeline/BaseBatch.BitmapCommands.cs | 4 ++ .../Pipeline/IBatchBitmapCommands.cs | 4 ++ .../BitmapCommandTests.cs | 48 +++++++++++++++++++ 6 files changed, 94 insertions(+) diff --git a/sources/Valkey.Glide/BaseClient.BitmapCommands.cs b/sources/Valkey.Glide/BaseClient.BitmapCommands.cs index 3d43ef41..67e300e6 100644 --- a/sources/Valkey.Glide/BaseClient.BitmapCommands.cs +++ b/sources/Valkey.Glide/BaseClient.BitmapCommands.cs @@ -20,4 +20,11 @@ public async Task StringSetBitAsync(ValkeyKey key, long offset, bool value Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); return await Command(Request.SetBitAsync(key, offset, value)); } + + /// + public async Task StringBitCountAsync(ValkeyKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.BitCountAsync(key, start, end, indexType)); + } } \ No newline at end of file diff --git a/sources/Valkey.Glide/Commands/IBitmapCommands.cs b/sources/Valkey.Glide/Commands/IBitmapCommands.cs index 9901cc78..30ee0559 100644 --- a/sources/Valkey.Glide/Commands/IBitmapCommands.cs +++ b/sources/Valkey.Glide/Commands/IBitmapCommands.cs @@ -46,4 +46,25 @@ public interface IBitmapCommands /// /// Task StringSetBitAsync(ValkeyKey key, long offset, bool value, CommandFlags flags = CommandFlags.None); + + /// + /// Count the number of set bits in a string. + /// + /// + /// The key of the string. + /// The start offset. + /// The end offset. + /// The index type (bit or byte). + /// The flags to use for this operation. Currently flags are ignored. + /// The number of bits set to 1. + /// + /// + /// + /// await client.StringSetAsync("mykey", "A"); // ASCII 'A' is 01000001 + /// long count = await client.StringBitCountAsync("mykey"); + /// Console.WriteLine(count); // Output: 2 (two bits set) + /// + /// + /// + Task StringBitCountAsync(ValkeyKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None); } \ No newline at end of file diff --git a/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs b/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs index 4ccb1c2e..d2f12ccf 100644 --- a/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs @@ -11,4 +11,14 @@ public static Cmd GetBitAsync(ValkeyKey key, long offset) public static Cmd SetBitAsync(ValkeyKey key, long offset, bool value) => new(RequestType.SetBit, [key.ToGlideString(), offset.ToGlideString(), (value ? 1 : 0).ToGlideString()], false, response => (long)response != 0); + + public static Cmd BitCountAsync(ValkeyKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte) + { + List args = [key.ToGlideString(), start.ToGlideString(), end.ToGlideString()]; + if (indexType != StringIndexType.Byte) + { + args.Add(indexType.ToLiteral().ToGlideString()); + } + return Simple(RequestType.BitCount, [.. args]); + } } \ No newline at end of file diff --git a/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs b/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs index 6acaf403..50016365 100644 --- a/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs +++ b/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs @@ -12,6 +12,10 @@ public abstract partial class BaseBatch where T : BaseBatch /// public T StringSetBitAsync(ValkeyKey key, long offset, bool value) => AddCmd(Request.SetBitAsync(key, offset, value)); + /// + public T StringBitCountAsync(ValkeyKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte) => AddCmd(Request.BitCountAsync(key, start, end, indexType)); + IBatch IBatchBitmapCommands.StringGetBit(ValkeyKey key, long offset) => StringGetBitAsync(key, offset); IBatch IBatchBitmapCommands.StringSetBit(ValkeyKey key, long offset, bool value) => StringSetBitAsync(key, offset, value); + IBatch IBatchBitmapCommands.StringBitCount(ValkeyKey key, long start, long end, StringIndexType indexType) => StringBitCountAsync(key, start, end, indexType); } \ No newline at end of file diff --git a/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs b/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs index e8efed03..722c6b86 100644 --- a/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs +++ b/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs @@ -14,4 +14,8 @@ internal interface IBatchBitmapCommands /// /// Command Response - IBatch StringSetBit(ValkeyKey key, long offset, bool value); + + /// + /// Command Response - + IBatch StringBitCount(ValkeyKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte); } \ No newline at end of file diff --git a/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs index 80adf6cc..3c72c51c 100644 --- a/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs @@ -130,4 +130,52 @@ public async Task GetSetBit_CombinedOperations_WorksTogether(BaseClient client) Assert.False(finalBit0); Assert.True(finalBit1); } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task BitCount_CountsSetBits(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Set string to "A" (ASCII 65 = 01000001 in binary = 2 bits set) + await client.StringSetAsync(key, "A"); + + // Count all bits + long count = await client.StringBitCountAsync(key); + Assert.Equal(2, count); + + // Count bits in byte range + long countRange = await client.StringBitCountAsync(key, 0, 0); + Assert.Equal(2, countRange); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task BitCount_NonExistentKey_ReturnsZero(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + long count = await client.StringBitCountAsync(key); + Assert.Equal(0, count); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task BitCount_WithBitIndex_CountsCorrectly(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Set multiple bits + await client.StringSetBitAsync(key, 0, true); // bit 0 + await client.StringSetBitAsync(key, 1, true); // bit 1 + await client.StringSetBitAsync(key, 8, true); // bit 8 (second byte) + + // Count all bits + long totalCount = await client.StringBitCountAsync(key); + Assert.Equal(3, totalCount); + + // Count bits 0-7 (first byte) using bit indexing + long firstByteCount = await client.StringBitCountAsync(key, 0, 7, StringIndexType.Bit); + Assert.Equal(2, firstByteCount); + } } \ No newline at end of file From 41354dc8ff7405bb6f8901972ca2699c478b58b0 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Wed, 15 Oct 2025 15:26:07 -0700 Subject: [PATCH 04/13] bitop Signed-off-by: Alex Rehnby-Martin --- .../Valkey.Glide/BaseClient.BitmapCommands.cs | 21 +++ .../Valkey.Glide/Commands/IBitmapCommands.cs | 66 +++++++ .../Internals/Request.BitmapCommands.cs | 23 +++ .../Pipeline/BaseBatch.BitmapCommands.cs | 12 ++ .../Pipeline/IBatchBitmapCommands.cs | 12 ++ .../BitmapCommandTests.cs | 162 ++++++++++++++++++ 6 files changed, 296 insertions(+) diff --git a/sources/Valkey.Glide/BaseClient.BitmapCommands.cs b/sources/Valkey.Glide/BaseClient.BitmapCommands.cs index 67e300e6..c62a61e6 100644 --- a/sources/Valkey.Glide/BaseClient.BitmapCommands.cs +++ b/sources/Valkey.Glide/BaseClient.BitmapCommands.cs @@ -27,4 +27,25 @@ public async Task StringBitCountAsync(ValkeyKey key, long start = 0, long Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); return await Command(Request.BitCountAsync(key, start, end, indexType)); } + + /// + public async Task StringBitPositionAsync(ValkeyKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.BitPositionAsync(key, bit, start, end, indexType)); + } + + /// + public async Task StringBitOperationAsync(Bitwise operation, ValkeyKey destination, ValkeyKey first, ValkeyKey second, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.BitOperationAsync(operation, destination, first, second)); + } + + /// + public async Task StringBitOperationAsync(Bitwise operation, ValkeyKey destination, ValkeyKey[] keys, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.BitOperationAsync(operation, destination, keys)); + } } \ No newline at end of file diff --git a/sources/Valkey.Glide/Commands/IBitmapCommands.cs b/sources/Valkey.Glide/Commands/IBitmapCommands.cs index 30ee0559..354769ce 100644 --- a/sources/Valkey.Glide/Commands/IBitmapCommands.cs +++ b/sources/Valkey.Glide/Commands/IBitmapCommands.cs @@ -67,4 +67,70 @@ public interface IBitmapCommands /// /// Task StringBitCountAsync(ValkeyKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None); + + /// + /// Return the position of the first bit set to 1 or 0 in a string. + /// + /// + /// The key of the string. + /// The bit value to search for (true for 1, false for 0). + /// The start offset. + /// The end offset. + /// The index type (bit or byte). + /// The flags to use for this operation. Currently flags are ignored. + /// The position of the first bit with the specified value, or -1 if not found. + /// + /// + /// + /// await client.StringSetAsync("mykey", "A"); // ASCII 'A' is 01000001 + /// long pos = await client.StringBitPositionAsync("mykey", true); + /// Console.WriteLine(pos); // Output: 1 (first set bit at position 1) + /// + /// + /// + Task StringBitPositionAsync(ValkeyKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None); + + /// + /// Perform a bitwise operation between multiple keys and store the result in the destination key. + /// + /// + /// The bitwise operation to perform. + /// The key to store the result. + /// The first source key. + /// The second source key. + /// The flags to use for this operation. Currently flags are ignored. + /// The size of the string stored in the destination key. + /// + /// + /// + /// await client.StringSetAsync("key1", "A"); + /// await client.StringSetAsync("key2", "B"); + /// long size = await client.StringBitOperationAsync(Bitwise.And, "result", "key1", "key2"); + /// Console.WriteLine(size); // Output: 1 (size of result) + /// + /// + /// + Task StringBitOperationAsync(Bitwise operation, ValkeyKey destination, ValkeyKey first, ValkeyKey second, CommandFlags flags = CommandFlags.None); + + /// + /// Perform a bitwise operation between multiple keys and store the result in the destination key. + /// + /// + /// The bitwise operation to perform. + /// The key to store the result. + /// The source keys. + /// The flags to use for this operation. Currently flags are ignored. + /// The size of the string stored in the destination key. + /// + /// + /// + /// await client.StringSetAsync("key1", "A"); + /// await client.StringSetAsync("key2", "B"); + /// await client.StringSetAsync("key3", "C"); + /// long size = await client.StringBitOperationAsync(Bitwise.Or, "result", new ValkeyKey[] { "key1", "key2", "key3" }); + /// Console.WriteLine(size); // Output: 1 (size of result) + /// + /// + /// + Task StringBitOperationAsync(Bitwise operation, ValkeyKey destination, ValkeyKey[] keys, CommandFlags flags = CommandFlags.None); } \ No newline at end of file diff --git a/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs b/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs index d2f12ccf..ddf46ca1 100644 --- a/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs @@ -21,4 +21,27 @@ public static Cmd BitCountAsync(ValkeyKey key, long start = 0, long } return Simple(RequestType.BitCount, [.. args]); } + + public static Cmd BitPositionAsync(ValkeyKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte) + { + List args = [key.ToGlideString(), (bit ? 1 : 0).ToGlideString(), start.ToGlideString(), end.ToGlideString()]; + if (indexType != StringIndexType.Byte) + { + args.Add(indexType.ToLiteral().ToGlideString()); + } + return Simple(RequestType.BitPos, [.. args]); + } + + public static Cmd BitOperationAsync(Bitwise operation, ValkeyKey destination, ValkeyKey first, ValkeyKey second) + { + GlideString[] args = [ValkeyLiterals.Get(operation).ToGlideString(), destination.ToGlideString(), first.ToGlideString(), second.ToGlideString()]; + return Simple(RequestType.BitOp, args); + } + + public static Cmd BitOperationAsync(Bitwise operation, ValkeyKey destination, ValkeyKey[] keys) + { + List args = [ValkeyLiterals.Get(operation).ToGlideString(), destination.ToGlideString()]; + args.AddRange(keys.ToGlideStrings()); + return Simple(RequestType.BitOp, [.. args]); + } } \ No newline at end of file diff --git a/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs b/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs index 50016365..ec36b2e7 100644 --- a/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs +++ b/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs @@ -15,7 +15,19 @@ public abstract partial class BaseBatch where T : BaseBatch /// public T StringBitCountAsync(ValkeyKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte) => AddCmd(Request.BitCountAsync(key, start, end, indexType)); + /// + public T StringBitPositionAsync(ValkeyKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte) => AddCmd(Request.BitPositionAsync(key, bit, start, end, indexType)); + + /// + public T StringBitOperationAsync(Bitwise operation, ValkeyKey destination, ValkeyKey first, ValkeyKey second) => AddCmd(Request.BitOperationAsync(operation, destination, first, second)); + + /// + public T StringBitOperationAsync(Bitwise operation, ValkeyKey destination, ValkeyKey[] keys) => AddCmd(Request.BitOperationAsync(operation, destination, keys)); + IBatch IBatchBitmapCommands.StringGetBit(ValkeyKey key, long offset) => StringGetBitAsync(key, offset); IBatch IBatchBitmapCommands.StringSetBit(ValkeyKey key, long offset, bool value) => StringSetBitAsync(key, offset, value); IBatch IBatchBitmapCommands.StringBitCount(ValkeyKey key, long start, long end, StringIndexType indexType) => StringBitCountAsync(key, start, end, indexType); + IBatch IBatchBitmapCommands.StringBitPosition(ValkeyKey key, bool bit, long start, long end, StringIndexType indexType) => StringBitPositionAsync(key, bit, start, end, indexType); + IBatch IBatchBitmapCommands.StringBitOperation(Bitwise operation, ValkeyKey destination, ValkeyKey first, ValkeyKey second) => StringBitOperationAsync(operation, destination, first, second); + IBatch IBatchBitmapCommands.StringBitOperation(Bitwise operation, ValkeyKey destination, ValkeyKey[] keys) => StringBitOperationAsync(operation, destination, keys); } \ No newline at end of file diff --git a/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs b/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs index 722c6b86..e8fd649b 100644 --- a/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs +++ b/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs @@ -18,4 +18,16 @@ internal interface IBatchBitmapCommands /// /// Command Response - IBatch StringBitCount(ValkeyKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte); + + /// + /// Command Response - + IBatch StringBitPosition(ValkeyKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte); + + /// + /// Command Response - + IBatch StringBitOperation(Bitwise operation, ValkeyKey destination, ValkeyKey first, ValkeyKey second); + + /// + /// Command Response - + IBatch StringBitOperation(Bitwise operation, ValkeyKey destination, ValkeyKey[] keys); } \ No newline at end of file diff --git a/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs index 3c72c51c..8a41114a 100644 --- a/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs @@ -178,4 +178,166 @@ public async Task BitCount_WithBitIndex_CountsCorrectly(BaseClient client) long firstByteCount = await client.StringBitCountAsync(key, 0, 7, StringIndexType.Bit); Assert.Equal(2, firstByteCount); } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task BitPosition_FindsFirstSetBit(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Set string to "A" (ASCII 65 = 01000001 in binary) + await client.StringSetAsync(key, "A"); + + // Find first set bit (should be at position 1) + long pos1 = await client.StringBitPositionAsync(key, true); + Assert.Equal(1, pos1); + + // Find first unset bit (should be at position 0) + long pos0 = await client.StringBitPositionAsync(key, false); + Assert.Equal(0, pos0); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task BitPosition_NonExistentKey_ReturnsMinusOne(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Search for set bit in non-existent key + long pos = await client.StringBitPositionAsync(key, true); + Assert.Equal(-1, pos); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task BitPosition_WithRange_FindsInRange(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Set multiple bits: bit 1 and bit 9 + await client.StringSetBitAsync(key, 1, true); + await client.StringSetBitAsync(key, 9, true); + + // Find first set bit in entire string + long pos1 = await client.StringBitPositionAsync(key, true); + Assert.Equal(1, pos1); + + // Find first set bit starting from bit 8 using bit indexing + long pos2 = await client.StringBitPositionAsync(key, true, 8, -1, StringIndexType.Bit); + Assert.Equal(9, pos2); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task BitOperation_And_PerformsCorrectOperation(BaseClient client) + { + string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; + string key1 = keyPrefix + ":key1"; + string key2 = keyPrefix + ":key2"; + string result = keyPrefix + ":result"; + + // Set key1 to "A" (01000001) and key2 to "B" (01000010) + await client.StringSetAsync(key1, "A"); + await client.StringSetAsync(key2, "B"); + + // Perform AND operation + long size = await client.StringBitOperationAsync(Bitwise.And, result, key1, key2); + Assert.Equal(1, size); + + // Verify result: A AND B = 01000001 AND 01000010 = 01000000 = '@' + ValkeyValue resultValue = await client.StringGetAsync(result); + Assert.Equal("@", resultValue.ToString()); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task BitOperation_Or_PerformsCorrectOperation(BaseClient client) + { + string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; + string key1 = keyPrefix + ":key1"; + string key2 = keyPrefix + ":key2"; + string result = keyPrefix + ":result"; + + // Set key1 to "A" (01000001) and key2 to "B" (01000010) + await client.StringSetAsync(key1, "A"); + await client.StringSetAsync(key2, "B"); + + // Perform OR operation + long size = await client.StringBitOperationAsync(Bitwise.Or, result, key1, key2); + Assert.Equal(1, size); + + // Verify result: A OR B = 01000001 OR 01000010 = 01000011 = 'C' + ValkeyValue resultValue = await client.StringGetAsync(result); + Assert.Equal("C", resultValue.ToString()); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task BitOperation_MultipleKeys_PerformsCorrectOperation(BaseClient client) + { + string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; + string key1 = keyPrefix + ":key1"; + string key2 = keyPrefix + ":key2"; + string key3 = keyPrefix + ":key3"; + string result = keyPrefix + ":result"; + + // Set keys with different bit patterns + await client.StringSetAsync(key1, "A"); // 01000001 + await client.StringSetAsync(key2, "B"); // 01000010 + await client.StringSetAsync(key3, "D"); // 01000100 + + // Perform OR operation on multiple keys + ValkeyKey[] keys = [key1, key2, key3]; + long size = await client.StringBitOperationAsync(Bitwise.Or, result, keys); + Assert.Equal(1, size); + + // Verify result: A OR B OR D = 01000001 OR 01000010 OR 01000100 = 01000111 = 'G' + ValkeyValue resultValue = await client.StringGetAsync(result); + Assert.Equal("G", resultValue.ToString()); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task BitOperation_Xor_PerformsCorrectOperation(BaseClient client) + { + string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; + string key1 = keyPrefix + ":key1"; + string key2 = keyPrefix + ":key2"; + string result = keyPrefix + ":result"; + + // Set key1 to "A" (01000001) and key2 to "B" (01000010) + await client.StringSetAsync(key1, "A"); + await client.StringSetAsync(key2, "B"); + + // Perform XOR operation + long size = await client.StringBitOperationAsync(Bitwise.Xor, result, key1, key2); + Assert.Equal(1, size); + + // Verify result: A XOR B = 01000001 XOR 01000010 = 00000011 = ASCII 3 + ValkeyValue resultValue = await client.StringGetAsync(result); + byte[] resultBytes = resultValue; + Assert.Equal(3, resultBytes[0]); // XOR of 65 (A) and 66 (B) is 3 + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task BitOperation_Not_PerformsCorrectOperation(BaseClient client) + { + string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; + string key1 = keyPrefix + ":key1"; + string result = keyPrefix + ":result"; + + // Set key1 to "A" (01000001) + await client.StringSetAsync(key1, "A"); + + // Perform NOT operation (NOT only takes one key) + ValkeyKey[] keys = [key1]; + long size = await client.StringBitOperationAsync(Bitwise.Not, result, keys); + Assert.Equal(1, size); + + // Verify result: NOT A = NOT 01000001 = 10111110 = '¾' (ASCII 190) + ValkeyValue resultValue = await client.StringGetAsync(result); + byte[] resultBytes = resultValue; + Assert.Equal(190, resultBytes[0]); // NOT of 65 (A) is 190 + } } \ No newline at end of file From 7d791df23179f2861cb95c70b6f9326fe8121568 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Mon, 20 Oct 2025 15:22:10 -0700 Subject: [PATCH 05/13] Implement bitfield Signed-off-by: Alex Rehnby-Martin --- .../Valkey.Glide/BaseClient.BitmapCommands.cs | 15 ++ .../Valkey.Glide/Commands/IBitmapCommands.cs | 47 +++++ .../Commands/Options/BitFieldOptions.cs | 173 ++++++++++++++++++ .../Internals/Request.BitmapCommands.cs | 23 +++ .../Pipeline/BaseBatch.BitmapCommands.cs | 9 + .../Pipeline/IBatchBitmapCommands.cs | 8 + .../BitmapCommandTests.cs | 146 +++++++++++++++ 7 files changed, 421 insertions(+) create mode 100644 sources/Valkey.Glide/Commands/Options/BitFieldOptions.cs diff --git a/sources/Valkey.Glide/BaseClient.BitmapCommands.cs b/sources/Valkey.Glide/BaseClient.BitmapCommands.cs index c62a61e6..c56d2ff0 100644 --- a/sources/Valkey.Glide/BaseClient.BitmapCommands.cs +++ b/sources/Valkey.Glide/BaseClient.BitmapCommands.cs @@ -1,6 +1,7 @@ // Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 using Valkey.Glide.Commands; +using Valkey.Glide.Commands.Options; using Valkey.Glide.Internals; namespace Valkey.Glide; @@ -48,4 +49,18 @@ public async Task StringBitOperationAsync(Bitwise operation, ValkeyKey des Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); return await Command(Request.BitOperationAsync(operation, destination, keys)); } + + /// + public async Task StringBitFieldAsync(ValkeyKey key, BitFieldOptions.IBitFieldSubCommand[] subCommands, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.BitFieldAsync(key, subCommands)); + } + + /// + public async Task StringBitFieldReadOnlyAsync(ValkeyKey key, BitFieldOptions.IBitFieldReadOnlySubCommand[] subCommands, CommandFlags flags = CommandFlags.None) + { + Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + return await Command(Request.BitFieldReadOnlyAsync(key, subCommands)); + } } \ No newline at end of file diff --git a/sources/Valkey.Glide/Commands/IBitmapCommands.cs b/sources/Valkey.Glide/Commands/IBitmapCommands.cs index 354769ce..e96945a6 100644 --- a/sources/Valkey.Glide/Commands/IBitmapCommands.cs +++ b/sources/Valkey.Glide/Commands/IBitmapCommands.cs @@ -133,4 +133,51 @@ public interface IBitmapCommands /// /// Task StringBitOperationAsync(Bitwise operation, ValkeyKey destination, ValkeyKey[] keys, CommandFlags flags = CommandFlags.None); + + /// + /// Reads or modifies the array of bits representing the string stored at key based on the specified subcommands. + /// + /// + /// The key of the string. + /// The subcommands to execute (GET, SET, INCRBY). + /// The flags to use for this operation. Currently flags are ignored. + /// An array of results from the executed subcommands. + /// + /// + /// + /// await client.StringSetAsync("mykey", "A"); // ASCII 'A' is 01000001 + /// var subCommands = new IBitFieldSubCommand[] { + /// new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), 0), + /// new BitFieldOptions.BitFieldSet(BitFieldOptions.Encoding.Unsigned(8), 0, 66) // ASCII 'B' + /// }; + /// long[] results = await client.StringBitFieldAsync("mykey", subCommands); + /// Console.WriteLine(results[0]); // Output: 65 (ASCII 'A') + /// Console.WriteLine(results[1]); // Output: 65 (old value) + /// + /// + /// + Task StringBitFieldAsync(ValkeyKey key, Commands.Options.BitFieldOptions.IBitFieldSubCommand[] subCommands, CommandFlags flags = CommandFlags.None); + + /// + /// Reads the array of bits representing the string stored at key based on the specified GET subcommands. + /// This is a read-only variant of BITFIELD. + /// + /// + /// The key of the string. + /// The GET subcommands to execute. + /// The flags to use for this operation. Currently flags are ignored. + /// An array of results from the executed GET subcommands. + /// + /// + /// + /// await client.StringSetAsync("mykey", "A"); // ASCII 'A' is 01000001 + /// var subCommands = new IBitFieldReadOnlySubCommand[] { + /// new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), 0) + /// }; + /// long[] results = await client.StringBitFieldReadOnlyAsync("mykey", subCommands); + /// Console.WriteLine(results[0]); // Output: 65 (ASCII 'A') + /// + /// + /// + Task StringBitFieldReadOnlyAsync(ValkeyKey key, Commands.Options.BitFieldOptions.IBitFieldReadOnlySubCommand[] subCommands, CommandFlags flags = CommandFlags.None); } \ No newline at end of file diff --git a/sources/Valkey.Glide/Commands/Options/BitFieldOptions.cs b/sources/Valkey.Glide/Commands/Options/BitFieldOptions.cs new file mode 100644 index 00000000..a8847625 --- /dev/null +++ b/sources/Valkey.Glide/Commands/Options/BitFieldOptions.cs @@ -0,0 +1,173 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +namespace Valkey.Glide.Commands.Options; + +/// +/// Options for BITFIELD command operations. +/// +public static class BitFieldOptions +{ + /// + /// Base interface for BitField subcommands. + /// + public interface IBitFieldSubCommand + { + /// + /// Converts the subcommand to its string arguments. + /// + /// Array of string arguments for the subcommand. + string[] ToArgs(); + } + + /// + /// Interface for read-only BitField subcommands. + /// + public interface IBitFieldReadOnlySubCommand : IBitFieldSubCommand + { + } + + /// + /// Interface for bit field offsets. + /// + public interface IBitOffset + { + string GetOffset(); + } + + /// + /// Regular bit offset. + /// + public class BitOffset : IBitOffset + { + private readonly long _offset; + + public BitOffset(long offset) => _offset = offset; + public string GetOffset() => _offset.ToString(); + } + + /// + /// Offset multiplied by encoding width (prefixed with #). + /// + public class BitOffsetMultiplier : IBitOffset + { + private readonly long _multiplier; + + public BitOffsetMultiplier(long multiplier) => _multiplier = multiplier; + public string GetOffset() => $"#{_multiplier}"; + } + + /// + /// GET subcommand for reading bits from the string. + /// + public class BitFieldGet : IBitFieldReadOnlySubCommand + { + private readonly string _encoding; + private readonly IBitOffset _offset; + + public BitFieldGet(string encoding, IBitOffset offset) + { + _encoding = encoding; + _offset = offset; + } + + public string[] ToArgs() => ["GET", _encoding, _offset.GetOffset()]; + } + + /// + /// SET subcommand for setting bits in the string. + /// + public class BitFieldSet : IBitFieldSubCommand + { + private readonly string _encoding; + private readonly IBitOffset _offset; + private readonly long _value; + + public BitFieldSet(string encoding, IBitOffset offset, long value) + { + _encoding = encoding; + _offset = offset; + _value = value; + } + + public string[] ToArgs() => ["SET", _encoding, _offset.GetOffset(), _value.ToString()]; + } + + /// + /// INCRBY subcommand for incrementing bits in the string. + /// + public class BitFieldIncrBy : IBitFieldSubCommand + { + private readonly string _encoding; + private readonly IBitOffset _offset; + private readonly long _increment; + + public BitFieldIncrBy(string encoding, IBitOffset offset, long increment) + { + _encoding = encoding; + _offset = offset; + _increment = increment; + } + + public string[] ToArgs() => ["INCRBY", _encoding, _offset.GetOffset(), _increment.ToString()]; + } + + /// + /// OVERFLOW subcommand for controlling overflow behavior. + /// + public class BitFieldOverflow : IBitFieldSubCommand + { + private readonly OverflowType _overflowType; + + /// + /// Creates an OVERFLOW subcommand. + /// + /// The overflow behavior type. + public BitFieldOverflow(OverflowType overflowType) + { + _overflowType = overflowType; + } + + public string[] ToArgs() => ["OVERFLOW", _overflowType.ToString().ToUpper()]; + } + + /// + /// Overflow behavior types for BitField operations. + /// + public enum OverflowType + { + /// + /// Wrap around on overflow (modulo arithmetic). + /// + Wrap, + /// + /// Saturate at min/max values on overflow. + /// + Sat, + /// + /// Return null on overflow. + /// + Fail + } + + /// + /// Helper methods for creating common encodings. + /// + public static class Encoding + { + /// + /// Creates an unsigned encoding string. + /// + /// Number of bits (1-63). + /// Unsigned encoding string (e.g., "u8"). + public static string Unsigned(int bits) => $"u{bits}"; + + /// + /// Creates a signed encoding string. + /// + /// Number of bits (1-64). + /// Signed encoding string (e.g., "i8"). + public static string Signed(int bits) => $"i{bits}"; + } + + +} \ No newline at end of file diff --git a/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs b/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs index ddf46ca1..b0a574e2 100644 --- a/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs @@ -1,5 +1,6 @@ // Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 +using Valkey.Glide.Commands.Options; using static Valkey.Glide.Internals.FFI; namespace Valkey.Glide.Internals; @@ -44,4 +45,26 @@ public static Cmd BitOperationAsync(Bitwise operation, ValkeyKey des args.AddRange(keys.ToGlideStrings()); return Simple(RequestType.BitOp, [.. args]); } + + public static Cmd BitFieldAsync(ValkeyKey key, BitFieldOptions.IBitFieldSubCommand[] subCommands) + { + List args = [key.ToGlideString()]; + foreach (var subCommand in subCommands) + { + args.AddRange(subCommand.ToArgs().Select(arg => arg.ToGlideString())); + } + return new(RequestType.BitField, [.. args], false, response => + response.Select(item => item is null ? 0L : Convert.ToInt64(item)).ToArray()); + } + + public static Cmd BitFieldReadOnlyAsync(ValkeyKey key, BitFieldOptions.IBitFieldReadOnlySubCommand[] subCommands) + { + List args = [key.ToGlideString()]; + foreach (var subCommand in subCommands) + { + args.AddRange(subCommand.ToArgs().Select(arg => arg.ToGlideString())); + } + return new(RequestType.BitFieldReadOnly, [.. args], false, response => + response.Select(item => item is null ? 0L : Convert.ToInt64(item)).ToArray()); + } } \ No newline at end of file diff --git a/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs b/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs index ec36b2e7..aa30f0ca 100644 --- a/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs +++ b/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs @@ -1,5 +1,6 @@ // Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 +using Valkey.Glide.Commands.Options; using Valkey.Glide.Internals; namespace Valkey.Glide.Pipeline; @@ -24,10 +25,18 @@ public abstract partial class BaseBatch where T : BaseBatch /// public T StringBitOperationAsync(Bitwise operation, ValkeyKey destination, ValkeyKey[] keys) => AddCmd(Request.BitOperationAsync(operation, destination, keys)); + /// + public T StringBitFieldAsync(ValkeyKey key, BitFieldOptions.IBitFieldSubCommand[] subCommands) => AddCmd(Request.BitFieldAsync(key, subCommands)); + + /// + public T StringBitFieldReadOnlyAsync(ValkeyKey key, BitFieldOptions.IBitFieldReadOnlySubCommand[] subCommands) => AddCmd(Request.BitFieldReadOnlyAsync(key, subCommands)); + IBatch IBatchBitmapCommands.StringGetBit(ValkeyKey key, long offset) => StringGetBitAsync(key, offset); IBatch IBatchBitmapCommands.StringSetBit(ValkeyKey key, long offset, bool value) => StringSetBitAsync(key, offset, value); IBatch IBatchBitmapCommands.StringBitCount(ValkeyKey key, long start, long end, StringIndexType indexType) => StringBitCountAsync(key, start, end, indexType); IBatch IBatchBitmapCommands.StringBitPosition(ValkeyKey key, bool bit, long start, long end, StringIndexType indexType) => StringBitPositionAsync(key, bit, start, end, indexType); IBatch IBatchBitmapCommands.StringBitOperation(Bitwise operation, ValkeyKey destination, ValkeyKey first, ValkeyKey second) => StringBitOperationAsync(operation, destination, first, second); IBatch IBatchBitmapCommands.StringBitOperation(Bitwise operation, ValkeyKey destination, ValkeyKey[] keys) => StringBitOperationAsync(operation, destination, keys); + IBatch IBatchBitmapCommands.StringBitField(ValkeyKey key, BitFieldOptions.IBitFieldSubCommand[] subCommands) => StringBitFieldAsync(key, subCommands); + IBatch IBatchBitmapCommands.StringBitFieldReadOnly(ValkeyKey key, BitFieldOptions.IBitFieldReadOnlySubCommand[] subCommands) => StringBitFieldReadOnlyAsync(key, subCommands); } \ No newline at end of file diff --git a/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs b/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs index e8fd649b..1a2031bd 100644 --- a/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs +++ b/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs @@ -30,4 +30,12 @@ internal interface IBatchBitmapCommands /// /// Command Response - IBatch StringBitOperation(Bitwise operation, ValkeyKey destination, ValkeyKey[] keys); + + /// + /// Command Response - + IBatch StringBitField(ValkeyKey key, Commands.Options.BitFieldOptions.IBitFieldSubCommand[] subCommands); + + /// + /// Command Response - + IBatch StringBitFieldReadOnly(ValkeyKey key, Commands.Options.BitFieldOptions.IBitFieldReadOnlySubCommand[] subCommands); } \ No newline at end of file diff --git a/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs index 8a41114a..6593478d 100644 --- a/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs @@ -1,5 +1,7 @@ // Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 +using Valkey.Glide.Commands.Options; + namespace Valkey.Glide.IntegrationTests; public class BitmapCommandTests(TestConfiguration config) @@ -340,4 +342,148 @@ public async Task BitOperation_Not_PerformsCorrectOperation(BaseClient client) byte[] resultBytes = resultValue; Assert.Equal(190, resultBytes[0]); // NOT of 65 (A) is 190 } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task BitField_GetSetIncrBy_WorksCorrectly(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Set initial value "A" (ASCII 65 = 01000001) + await client.StringSetAsync(key, "A"); + + var subCommands = new BitFieldOptions.IBitFieldSubCommand[] + { + // Get 8 unsigned bits at offset 0 + new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0)), + // Set 8 unsigned bits at offset 0 to 66 (ASCII 'B') + new BitFieldOptions.BitFieldSet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 66), + // Increment by 1 + new BitFieldOptions.BitFieldIncrBy(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 1) + }; + + long[] results = await client.StringBitFieldAsync(key, subCommands); + + Assert.Equal(3, results.Length); + Assert.Equal(65, results[0]); // Original value 'A' + Assert.Equal(65, results[1]); // Old value before SET + Assert.Equal(67, results[2]); // New value after INCRBY (66 + 1 = 67 = 'C') + + // Verify final value + ValkeyValue finalValue = await client.StringGetAsync(key); + Assert.Equal("C", finalValue.ToString()); + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task BitFieldReadOnly_Get_WorksCorrectly(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Set initial value "A" (ASCII 65 = 01000001) + await client.StringSetAsync(key, "A"); + + var readOnlyCommands = new BitFieldOptions.IBitFieldReadOnlySubCommand[] + { + new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0)), + new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(4), new BitFieldOptions.BitOffset(0)), + new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(4), new BitFieldOptions.BitOffset(4)) + }; + + long[] results = await client.StringBitFieldReadOnlyAsync(key, readOnlyCommands); + + Assert.Equal(3, results.Length); + Assert.Equal(65, results[0]); // Full 8 bits: 01000001 = 65 + Assert.Equal(4, results[1]); // First 4 bits: 0100 = 4 + Assert.Equal(1, results[2]); // Next 4 bits: 0001 = 1 + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task BitField_OverflowControl_WorksCorrectly(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + var subCommands = new BitFieldOptions.IBitFieldSubCommand[] + { + // Set overflow to WRAP first + new BitFieldOptions.BitFieldOverflow(BitFieldOptions.OverflowType.Wrap), + // Set u8 at offset 0 to 255 (max value) + new BitFieldOptions.BitFieldSet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 255), + // Increment u8 at offset 0 by 1 (255 + 1 = 0 with wrap) + new BitFieldOptions.BitFieldIncrBy(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 1) + }; + + long[] results = await client.StringBitFieldAsync(key, subCommands); + + Assert.Equal(2, results.Length); + Assert.Equal(0, results[0]); // SET returns old value (0) + Assert.Equal(0, results[1]); // 255 + 1 = 0 (wrapped) + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task BitField_OverflowSat_WorksCorrectly(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + var subCommands = new BitFieldOptions.IBitFieldSubCommand[] + { + new BitFieldOptions.BitFieldOverflow(BitFieldOptions.OverflowType.Sat), + new BitFieldOptions.BitFieldSet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 250), + new BitFieldOptions.BitFieldIncrBy(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 10) + }; + + long[] results = await client.StringBitFieldAsync(key, subCommands); + + Assert.Equal(2, results.Length); + Assert.Equal(0, results[0]); // SET returns old value (0) + Assert.Equal(255, results[1]); // 250 + 10 = 255 (saturated at max) + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task BitField_OverflowFail_WorksCorrectly(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + var subCommands = new BitFieldOptions.IBitFieldSubCommand[] + { + new BitFieldOptions.BitFieldOverflow(BitFieldOptions.OverflowType.Fail), + new BitFieldOptions.BitFieldSet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 255), + new BitFieldOptions.BitFieldIncrBy(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 1) + }; + + long[] results = await client.StringBitFieldAsync(key, subCommands); + + Assert.Equal(2, results.Length); + Assert.Equal(0, results[0]); // SET returns old value (0) + Assert.Equal(0, results[1]); // 255 + 1 = null (fail), converted to 0 + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task BitField_OffsetMultiplier_WorksCorrectly(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + var subCommands = new BitFieldOptions.IBitFieldSubCommand[] + { + // Set first i8 at offset #0 (0 * 8 = bit 0) + new BitFieldOptions.BitFieldSet(BitFieldOptions.Encoding.Signed(8), new BitFieldOptions.BitOffsetMultiplier(0), 100), + // Set second i8 at offset #1 (1 * 8 = bit 8) + new BitFieldOptions.BitFieldSet(BitFieldOptions.Encoding.Signed(8), new BitFieldOptions.BitOffsetMultiplier(1), -50), + // Get both values back + new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Signed(8), new BitFieldOptions.BitOffsetMultiplier(0)), + new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Signed(8), new BitFieldOptions.BitOffsetMultiplier(1)) + }; + + long[] results = await client.StringBitFieldAsync(key, subCommands); + + Assert.Equal(4, results.Length); + Assert.Equal(0, results[0]); // SET returns old value (0) + Assert.Equal(0, results[1]); // SET returns old value (0) + Assert.Equal(100, results[2]); // First i8 value + Assert.Equal(-50, results[3]); // Second i8 value + } } \ No newline at end of file From be588528c7456e0ea4d73b57a32dda654752d0c0 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Fri, 31 Oct 2025 15:19:00 -0700 Subject: [PATCH 06/13] Batch and unit tests Signed-off-by: Alex Rehnby-Martin --- .../Valkey.Glide/BaseClient.BitmapCommands.cs | 13 +- .../Valkey.Glide/Commands/IBitmapCommands.cs | 2 +- .../Commands/Options/BitFieldOptions.cs | 85 +++----- .../Internals/Request.BitmapCommands.cs | 19 +- .../Pipeline/BaseBatch.BitmapCommands.cs | 2 +- .../Pipeline/IBatchBitmapCommands.cs | 2 +- .../BatchTestUtils.cs | 91 ++++++++- .../BitmapCommandTests.cs | 185 +++++++++++------- tests/Valkey.Glide.UnitTests/CommandTests.cs | 37 +++- 9 files changed, 286 insertions(+), 150 deletions(-) diff --git a/sources/Valkey.Glide/BaseClient.BitmapCommands.cs b/sources/Valkey.Glide/BaseClient.BitmapCommands.cs index c56d2ff0..aa9beec8 100644 --- a/sources/Valkey.Glide/BaseClient.BitmapCommands.cs +++ b/sources/Valkey.Glide/BaseClient.BitmapCommands.cs @@ -54,6 +54,17 @@ public async Task StringBitOperationAsync(Bitwise operation, ValkeyKey des public async Task StringBitFieldAsync(ValkeyKey key, BitFieldOptions.IBitFieldSubCommand[] subCommands, CommandFlags flags = CommandFlags.None) { Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); + + // Check if all subcommands are read-only (GET operations) + bool allReadOnly = subCommands.All(cmd => cmd is BitFieldOptions.IBitFieldReadOnlySubCommand); + + if (allReadOnly) + { + // Convert to read-only subcommands and use BITFIELD_RO + var readOnlyCommands = subCommands.Cast().ToArray(); + return await Command(Request.BitFieldReadOnlyAsync(key, readOnlyCommands)); + } + return await Command(Request.BitFieldAsync(key, subCommands)); } @@ -63,4 +74,4 @@ public async Task StringBitFieldReadOnlyAsync(ValkeyKey key, BitFieldOpt Utils.Requires(flags == CommandFlags.None, "Command flags are not supported by GLIDE"); return await Command(Request.BitFieldReadOnlyAsync(key, subCommands)); } -} \ No newline at end of file +} diff --git a/sources/Valkey.Glide/Commands/IBitmapCommands.cs b/sources/Valkey.Glide/Commands/IBitmapCommands.cs index e96945a6..b089b478 100644 --- a/sources/Valkey.Glide/Commands/IBitmapCommands.cs +++ b/sources/Valkey.Glide/Commands/IBitmapCommands.cs @@ -180,4 +180,4 @@ public interface IBitmapCommands /// /// Task StringBitFieldReadOnlyAsync(ValkeyKey key, Commands.Options.BitFieldOptions.IBitFieldReadOnlySubCommand[] subCommands, CommandFlags flags = CommandFlags.None); -} \ No newline at end of file +} diff --git a/sources/Valkey.Glide/Commands/Options/BitFieldOptions.cs b/sources/Valkey.Glide/Commands/Options/BitFieldOptions.cs index a8847625..1f638421 100644 --- a/sources/Valkey.Glide/Commands/Options/BitFieldOptions.cs +++ b/sources/Valkey.Glide/Commands/Options/BitFieldOptions.cs @@ -37,97 +37,60 @@ public interface IBitOffset /// /// Regular bit offset. /// - public class BitOffset : IBitOffset + /// The bit offset value. + public class BitOffset(long offset) : IBitOffset { - private readonly long _offset; - - public BitOffset(long offset) => _offset = offset; - public string GetOffset() => _offset.ToString(); + public string GetOffset() => offset.ToString(); } /// /// Offset multiplied by encoding width (prefixed with #). /// - public class BitOffsetMultiplier : IBitOffset + /// The multiplier value. + public class BitOffsetMultiplier(long multiplier) : IBitOffset { - private readonly long _multiplier; - - public BitOffsetMultiplier(long multiplier) => _multiplier = multiplier; - public string GetOffset() => $"#{_multiplier}"; + public string GetOffset() => $"#{multiplier}"; } /// /// GET subcommand for reading bits from the string. /// - public class BitFieldGet : IBitFieldReadOnlySubCommand + /// The bit field encoding. + /// The bit field offset. + public class BitFieldGet(string encoding, IBitOffset offset) : IBitFieldReadOnlySubCommand { - private readonly string _encoding; - private readonly IBitOffset _offset; - - public BitFieldGet(string encoding, IBitOffset offset) - { - _encoding = encoding; - _offset = offset; - } - - public string[] ToArgs() => ["GET", _encoding, _offset.GetOffset()]; + public string[] ToArgs() => ["GET", encoding, offset.GetOffset()]; } /// /// SET subcommand for setting bits in the string. /// - public class BitFieldSet : IBitFieldSubCommand + /// The bit field encoding. + /// The bit field offset. + /// The value to set. + public class BitFieldSet(string encoding, IBitOffset offset, long value) : IBitFieldSubCommand { - private readonly string _encoding; - private readonly IBitOffset _offset; - private readonly long _value; - - public BitFieldSet(string encoding, IBitOffset offset, long value) - { - _encoding = encoding; - _offset = offset; - _value = value; - } - - public string[] ToArgs() => ["SET", _encoding, _offset.GetOffset(), _value.ToString()]; + public string[] ToArgs() => ["SET", encoding, offset.GetOffset(), value.ToString()]; } /// /// INCRBY subcommand for incrementing bits in the string. /// - public class BitFieldIncrBy : IBitFieldSubCommand + /// The bit field encoding. + /// The bit field offset. + /// The increment value. + public class BitFieldIncrBy(string encoding, IBitOffset offset, long increment) : IBitFieldSubCommand { - private readonly string _encoding; - private readonly IBitOffset _offset; - private readonly long _increment; - - public BitFieldIncrBy(string encoding, IBitOffset offset, long increment) - { - _encoding = encoding; - _offset = offset; - _increment = increment; - } - - public string[] ToArgs() => ["INCRBY", _encoding, _offset.GetOffset(), _increment.ToString()]; + public string[] ToArgs() => ["INCRBY", encoding, offset.GetOffset(), increment.ToString()]; } /// /// OVERFLOW subcommand for controlling overflow behavior. /// - public class BitFieldOverflow : IBitFieldSubCommand + /// The overflow behavior type. + public class BitFieldOverflow(OverflowType overflowType) : IBitFieldSubCommand { - private readonly OverflowType _overflowType; - - /// - /// Creates an OVERFLOW subcommand. - /// - /// The overflow behavior type. - public BitFieldOverflow(OverflowType overflowType) - { - _overflowType = overflowType; - } - - public string[] ToArgs() => ["OVERFLOW", _overflowType.ToString().ToUpper()]; + public string[] ToArgs() => ["OVERFLOW", overflowType.ToString().ToUpper()]; } /// @@ -170,4 +133,4 @@ public static class Encoding } -} \ No newline at end of file +} diff --git a/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs b/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs index b0a574e2..f34b73c7 100644 --- a/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs @@ -1,6 +1,7 @@ // Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 using Valkey.Glide.Commands.Options; + using static Valkey.Glide.Internals.FFI; namespace Valkey.Glide.Internals; @@ -8,10 +9,10 @@ namespace Valkey.Glide.Internals; internal partial class Request { public static Cmd GetBitAsync(ValkeyKey key, long offset) - => new(RequestType.GetBit, [key.ToGlideString(), offset.ToGlideString()], false, response => (long)response != 0); + => new(RequestType.GetBit, [key.ToGlideString(), offset.ToGlideString()], false, response => response != 0); public static Cmd SetBitAsync(ValkeyKey key, long offset, bool value) - => new(RequestType.SetBit, [key.ToGlideString(), offset.ToGlideString(), (value ? 1 : 0).ToGlideString()], false, response => (long)response != 0); + => new(RequestType.SetBit, [key.ToGlideString(), offset.ToGlideString(), (value ? 1 : 0).ToGlideString()], false, response => response != 0); public static Cmd BitCountAsync(ValkeyKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte) { @@ -51,10 +52,10 @@ public static Cmd BitFieldAsync(ValkeyKey key, BitFieldOptions List args = [key.ToGlideString()]; foreach (var subCommand in subCommands) { - args.AddRange(subCommand.ToArgs().Select(arg => arg.ToGlideString())); + args.AddRange(subCommand.ToArgs().ToGlideStrings()); } - return new(RequestType.BitField, [.. args], false, response => - response.Select(item => item is null ? 0L : Convert.ToInt64(item)).ToArray()); + return new(RequestType.BitField, [.. args], false, response => + [.. response.Select(item => item is null ? 0L : Convert.ToInt64(item))]); } public static Cmd BitFieldReadOnlyAsync(ValkeyKey key, BitFieldOptions.IBitFieldReadOnlySubCommand[] subCommands) @@ -62,9 +63,9 @@ public static Cmd BitFieldReadOnlyAsync(ValkeyKey key, BitFiel List args = [key.ToGlideString()]; foreach (var subCommand in subCommands) { - args.AddRange(subCommand.ToArgs().Select(arg => arg.ToGlideString())); + args.AddRange(subCommand.ToArgs().ToGlideStrings()); } - return new(RequestType.BitFieldReadOnly, [.. args], false, response => - response.Select(item => item is null ? 0L : Convert.ToInt64(item)).ToArray()); + return new(RequestType.BitFieldReadOnly, [.. args], false, response => + [.. response.Select(item => item is null ? 0L : Convert.ToInt64(item))]); } -} \ No newline at end of file +} diff --git a/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs b/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs index aa30f0ca..0cdda6e1 100644 --- a/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs +++ b/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs @@ -39,4 +39,4 @@ public abstract partial class BaseBatch where T : BaseBatch IBatch IBatchBitmapCommands.StringBitOperation(Bitwise operation, ValkeyKey destination, ValkeyKey[] keys) => StringBitOperationAsync(operation, destination, keys); IBatch IBatchBitmapCommands.StringBitField(ValkeyKey key, BitFieldOptions.IBitFieldSubCommand[] subCommands) => StringBitFieldAsync(key, subCommands); IBatch IBatchBitmapCommands.StringBitFieldReadOnly(ValkeyKey key, BitFieldOptions.IBitFieldReadOnlySubCommand[] subCommands) => StringBitFieldReadOnlyAsync(key, subCommands); -} \ No newline at end of file +} diff --git a/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs b/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs index 1a2031bd..1a4f9db2 100644 --- a/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs +++ b/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs @@ -38,4 +38,4 @@ internal interface IBatchBitmapCommands /// /// Command Response - IBatch StringBitFieldReadOnly(ValkeyKey key, Commands.Options.BitFieldOptions.IBitFieldReadOnlySubCommand[] subCommands); -} \ No newline at end of file +} diff --git a/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs b/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs index 84e5d6b3..2ee02cc1 100644 --- a/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs +++ b/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs @@ -1,6 +1,6 @@ // Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 - +using Valkey.Glide.Commands.Options; namespace Valkey.Glide.IntegrationTests; @@ -1453,6 +1453,94 @@ public static List CreateGeospatialTest(Pipeline.IBatch batch, bool is return testData; } + public static List CreateBitmapTest(Pipeline.IBatch batch, bool isAtomic) + { + List testData = []; + string prefix = "{bitmapKey}-"; + string atomicPrefix = isAtomic ? prefix : ""; + string key1 = $"{atomicPrefix}1-{Guid.NewGuid()}"; + string key2 = $"{atomicPrefix}2-{Guid.NewGuid()}"; + string destKey = $"{atomicPrefix}dest-{Guid.NewGuid()}"; + + // Test StringSetBit and StringGetBit + _ = batch.StringSetBit(key1, 7, true); + testData.Add(new(false, "StringSetBit(key1, 7, true)")); + + _ = batch.StringGetBit(key1, 7); + testData.Add(new(true, "StringGetBit(key1, 7)")); + + _ = batch.StringSetBit(key1, 15, true); + testData.Add(new(false, "StringSetBit(key1, 15, true)")); + + _ = batch.StringGetBit(key1, 0); + testData.Add(new(false, "StringGetBit(key1, 0)")); + + // Test StringBitCount + _ = batch.StringBitCount(key1); + testData.Add(new(2L, "StringBitCount(key1)")); + + _ = batch.StringBitCount(key1, 0, 1); + testData.Add(new(2L, "StringBitCount(key1, 0, 1)")); + + // Test StringBitPosition + _ = batch.StringBitPosition(key1, true); + testData.Add(new(7L, "StringBitPosition(key1, true)")); + + _ = batch.StringBitPosition(key1, false); + testData.Add(new(0L, "StringBitPosition(key1, false)")); + + // Test StringBitOperation - use explicit prefix for cluster mode + string bitOpKey1 = $"{prefix}bitop1-{Guid.NewGuid()}"; + string bitOpKey2 = $"{prefix}bitop2-{Guid.NewGuid()}"; + string bitOpDest = $"{prefix}bitopdest-{Guid.NewGuid()}"; + + _ = batch.StringSetBit(bitOpKey1, 7, true); + testData.Add(new(false, "StringSetBit(bitOpKey1, 7, true)")); + + _ = batch.StringSetBit(bitOpKey1, 15, true); + testData.Add(new(false, "StringSetBit(bitOpKey1, 15, true)")); + + _ = batch.StringSetBit(bitOpKey2, 3, true); + testData.Add(new(false, "StringSetBit(bitOpKey2, 3, true)")); + + _ = batch.StringBitOperation(Bitwise.And, bitOpDest, bitOpKey1, bitOpKey2); + testData.Add(new(2L, "StringBitOperation(AND, bitOpDest, bitOpKey1, bitOpKey2)")); + + _ = batch.StringBitCount(bitOpDest); + testData.Add(new(0L, "StringBitCount(bitOpDest) after AND")); + + _ = batch.StringBitOperation(Bitwise.Or, bitOpDest, bitOpKey1, bitOpKey2); + testData.Add(new(2L, "StringBitOperation(OR, bitOpDest, bitOpKey1, bitOpKey2)")); + + _ = batch.StringBitCount(bitOpDest); + testData.Add(new(3L, "StringBitCount(bitOpDest) after OR")); + + // Test StringBitField - bit 7 set = value 1 in first byte + _ = batch.StringBitField(key1, [ + new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0)) + ]); + testData.Add(new(new long[] { 1L }, "StringBitField(key1, GET u8 0)")); + + _ = batch.StringBitField(key1, [ + new BitFieldOptions.BitFieldSet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(8), 255) + ]); + testData.Add(new(new long[] { 1L }, "StringBitField(key1, SET u8 8 255)")); + + _ = batch.StringBitField(key1, [ + new BitFieldOptions.BitFieldIncrBy(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(8), 1) + ]); + testData.Add(new(new long[] { 0L }, "StringBitField(key1, INCRBY u8 8 1)")); + + // Test StringBitFieldReadOnly + _ = batch.StringBitFieldReadOnly(key1, [ + new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0)), + new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(8)) + ]); + testData.Add(new(new long[] { 1L, 0L }, "StringBitFieldReadOnly(key1, GET u8 0, GET u8 8)")); + + return testData; + } + public static TheoryData GetTestClientWithAtomic => [.. TestConfiguration.TestClients.SelectMany(r => new[] { true, false }.SelectMany(isAtomic => new BatchTestData[] { @@ -1463,6 +1551,7 @@ [.. TestConfiguration.TestClients.SelectMany(r => new[] { true, false }.SelectMa new("List commands", r.Data, CreateListTest, isAtomic), new("Sorted Set commands", r.Data, CreateSortedSetTest, isAtomic), new("Geospatial commands", r.Data, CreateGeospatialTest, isAtomic), + new("Bitmap commands", r.Data, CreateBitmapTest, isAtomic), new("Connection Management commands", r.Data, CreateConnectionManagementTest, isAtomic), new("Server Management commands", r.Data, CreateServerManagementTest, isAtomic) }))]; diff --git a/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs index 6593478d..1ddaea0c 100644 --- a/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs @@ -13,17 +13,17 @@ public class BitmapCommandTests(TestConfiguration config) public async Task GetBit_ReturnsCorrectBitValue(BaseClient client) { string key = Guid.NewGuid().ToString(); - + // Set a string value - ASCII 'A' is 01000001 in binary await client.StringSetAsync(key, "A"); - + // Test bit positions in 'A' (01000001) bool bit0 = await client.StringGetBitAsync(key, 0); // Should be false (0) bool bit1 = await client.StringGetBitAsync(key, 1); // Should be true (1) bool bit2 = await client.StringGetBitAsync(key, 2); // Should be false (0) bool bit6 = await client.StringGetBitAsync(key, 6); // Should be false (0) bool bit7 = await client.StringGetBitAsync(key, 7); // Should be true (1) - + Assert.False(bit0); Assert.True(bit1); Assert.False(bit2); @@ -36,10 +36,10 @@ public async Task GetBit_ReturnsCorrectBitValue(BaseClient client) public async Task GetBit_NonExistentKey_ReturnsFalse(BaseClient client) { string key = Guid.NewGuid().ToString(); - + // Test bit on non-existent key bool bit = await client.StringGetBitAsync(key, 0); - + Assert.False(bit); } @@ -48,13 +48,13 @@ public async Task GetBit_NonExistentKey_ReturnsFalse(BaseClient client) public async Task GetBit_OffsetBeyondString_ReturnsFalse(BaseClient client) { string key = Guid.NewGuid().ToString(); - + // Set a short string await client.StringSetAsync(key, "A"); - + // Test bit beyond the string length bool bit = await client.StringGetBitAsync(key, 100); - + Assert.False(bit); } @@ -63,19 +63,19 @@ public async Task GetBit_OffsetBeyondString_ReturnsFalse(BaseClient client) public async Task SetBit_SetsAndReturnsOriginalValue(BaseClient client) { string key = Guid.NewGuid().ToString(); - + // Set bit 1 to true (original should be false) bool originalBit = await client.StringSetBitAsync(key, 1, true); Assert.False(originalBit); - + // Verify bit is now set bool currentBit = await client.StringGetBitAsync(key, 1); Assert.True(currentBit); - + // Set bit 1 to false (original should be true) bool originalBit2 = await client.StringSetBitAsync(key, 1, false); Assert.True(originalBit2); - + // Verify bit is now cleared bool currentBit2 = await client.StringGetBitAsync(key, 1); Assert.False(currentBit2); @@ -86,11 +86,11 @@ public async Task SetBit_SetsAndReturnsOriginalValue(BaseClient client) public async Task SetBit_NonExistentKey_CreatesKey(BaseClient client) { string key = Guid.NewGuid().ToString(); - + // Set bit on non-existent key bool originalBit = await client.StringSetBitAsync(key, 0, true); Assert.False(originalBit); - + // Verify key was created and bit is set bool currentBit = await client.StringGetBitAsync(key, 0); Assert.True(currentBit); @@ -101,31 +101,31 @@ public async Task SetBit_NonExistentKey_CreatesKey(BaseClient client) public async Task GetSetBit_CombinedOperations_WorksTogether(BaseClient client) { string key = Guid.NewGuid().ToString(); - + // Initially all bits should be 0 bool bit0 = await client.StringGetBitAsync(key, 0); bool bit1 = await client.StringGetBitAsync(key, 1); Assert.False(bit0); Assert.False(bit1); - + // Set bit 0 to 1 bool originalBit0 = await client.StringSetBitAsync(key, 0, true); Assert.False(originalBit0); // Was 0 - + // Set bit 1 to 1 bool originalBit1 = await client.StringSetBitAsync(key, 1, true); Assert.False(originalBit1); // Was 0 - + // Verify both bits are now set bool newBit0 = await client.StringGetBitAsync(key, 0); bool newBit1 = await client.StringGetBitAsync(key, 1); Assert.True(newBit0); Assert.True(newBit1); - + // Clear bit 0 bool clearBit0 = await client.StringSetBitAsync(key, 0, false); Assert.True(clearBit0); // Was 1 - + // Verify bit 0 is cleared but bit 1 remains set bool finalBit0 = await client.StringGetBitAsync(key, 0); bool finalBit1 = await client.StringGetBitAsync(key, 1); @@ -138,14 +138,14 @@ public async Task GetSetBit_CombinedOperations_WorksTogether(BaseClient client) public async Task BitCount_CountsSetBits(BaseClient client) { string key = Guid.NewGuid().ToString(); - + // Set string to "A" (ASCII 65 = 01000001 in binary = 2 bits set) await client.StringSetAsync(key, "A"); - + // Count all bits long count = await client.StringBitCountAsync(key); Assert.Equal(2, count); - + // Count bits in byte range long countRange = await client.StringBitCountAsync(key, 0, 0); Assert.Equal(2, countRange); @@ -156,7 +156,7 @@ public async Task BitCount_CountsSetBits(BaseClient client) public async Task BitCount_NonExistentKey_ReturnsZero(BaseClient client) { string key = Guid.NewGuid().ToString(); - + long count = await client.StringBitCountAsync(key); Assert.Equal(0, count); } @@ -166,16 +166,16 @@ public async Task BitCount_NonExistentKey_ReturnsZero(BaseClient client) public async Task BitCount_WithBitIndex_CountsCorrectly(BaseClient client) { string key = Guid.NewGuid().ToString(); - + // Set multiple bits await client.StringSetBitAsync(key, 0, true); // bit 0 await client.StringSetBitAsync(key, 1, true); // bit 1 await client.StringSetBitAsync(key, 8, true); // bit 8 (second byte) - + // Count all bits long totalCount = await client.StringBitCountAsync(key); Assert.Equal(3, totalCount); - + // Count bits 0-7 (first byte) using bit indexing long firstByteCount = await client.StringBitCountAsync(key, 0, 7, StringIndexType.Bit); Assert.Equal(2, firstByteCount); @@ -186,14 +186,14 @@ public async Task BitCount_WithBitIndex_CountsCorrectly(BaseClient client) public async Task BitPosition_FindsFirstSetBit(BaseClient client) { string key = Guid.NewGuid().ToString(); - + // Set string to "A" (ASCII 65 = 01000001 in binary) await client.StringSetAsync(key, "A"); - + // Find first set bit (should be at position 1) long pos1 = await client.StringBitPositionAsync(key, true); Assert.Equal(1, pos1); - + // Find first unset bit (should be at position 0) long pos0 = await client.StringBitPositionAsync(key, false); Assert.Equal(0, pos0); @@ -204,7 +204,7 @@ public async Task BitPosition_FindsFirstSetBit(BaseClient client) public async Task BitPosition_NonExistentKey_ReturnsMinusOne(BaseClient client) { string key = Guid.NewGuid().ToString(); - + // Search for set bit in non-existent key long pos = await client.StringBitPositionAsync(key, true); Assert.Equal(-1, pos); @@ -215,15 +215,15 @@ public async Task BitPosition_NonExistentKey_ReturnsMinusOne(BaseClient client) public async Task BitPosition_WithRange_FindsInRange(BaseClient client) { string key = Guid.NewGuid().ToString(); - + // Set multiple bits: bit 1 and bit 9 await client.StringSetBitAsync(key, 1, true); await client.StringSetBitAsync(key, 9, true); - + // Find first set bit in entire string long pos1 = await client.StringBitPositionAsync(key, true); Assert.Equal(1, pos1); - + // Find first set bit starting from bit 8 using bit indexing long pos2 = await client.StringBitPositionAsync(key, true, 8, -1, StringIndexType.Bit); Assert.Equal(9, pos2); @@ -237,15 +237,15 @@ public async Task BitOperation_And_PerformsCorrectOperation(BaseClient client) string key1 = keyPrefix + ":key1"; string key2 = keyPrefix + ":key2"; string result = keyPrefix + ":result"; - + // Set key1 to "A" (01000001) and key2 to "B" (01000010) await client.StringSetAsync(key1, "A"); await client.StringSetAsync(key2, "B"); - + // Perform AND operation long size = await client.StringBitOperationAsync(Bitwise.And, result, key1, key2); Assert.Equal(1, size); - + // Verify result: A AND B = 01000001 AND 01000010 = 01000000 = '@' ValkeyValue resultValue = await client.StringGetAsync(result); Assert.Equal("@", resultValue.ToString()); @@ -259,15 +259,15 @@ public async Task BitOperation_Or_PerformsCorrectOperation(BaseClient client) string key1 = keyPrefix + ":key1"; string key2 = keyPrefix + ":key2"; string result = keyPrefix + ":result"; - + // Set key1 to "A" (01000001) and key2 to "B" (01000010) await client.StringSetAsync(key1, "A"); await client.StringSetAsync(key2, "B"); - + // Perform OR operation long size = await client.StringBitOperationAsync(Bitwise.Or, result, key1, key2); Assert.Equal(1, size); - + // Verify result: A OR B = 01000001 OR 01000010 = 01000011 = 'C' ValkeyValue resultValue = await client.StringGetAsync(result); Assert.Equal("C", resultValue.ToString()); @@ -282,17 +282,17 @@ public async Task BitOperation_MultipleKeys_PerformsCorrectOperation(BaseClient string key2 = keyPrefix + ":key2"; string key3 = keyPrefix + ":key3"; string result = keyPrefix + ":result"; - + // Set keys with different bit patterns await client.StringSetAsync(key1, "A"); // 01000001 await client.StringSetAsync(key2, "B"); // 01000010 await client.StringSetAsync(key3, "D"); // 01000100 - + // Perform OR operation on multiple keys ValkeyKey[] keys = [key1, key2, key3]; long size = await client.StringBitOperationAsync(Bitwise.Or, result, keys); Assert.Equal(1, size); - + // Verify result: A OR B OR D = 01000001 OR 01000010 OR 01000100 = 01000111 = 'G' ValkeyValue resultValue = await client.StringGetAsync(result); Assert.Equal("G", resultValue.ToString()); @@ -306,18 +306,18 @@ public async Task BitOperation_Xor_PerformsCorrectOperation(BaseClient client) string key1 = keyPrefix + ":key1"; string key2 = keyPrefix + ":key2"; string result = keyPrefix + ":result"; - + // Set key1 to "A" (01000001) and key2 to "B" (01000010) await client.StringSetAsync(key1, "A"); await client.StringSetAsync(key2, "B"); - + // Perform XOR operation long size = await client.StringBitOperationAsync(Bitwise.Xor, result, key1, key2); Assert.Equal(1, size); - + // Verify result: A XOR B = 01000001 XOR 01000010 = 00000011 = ASCII 3 ValkeyValue resultValue = await client.StringGetAsync(result); - byte[] resultBytes = resultValue; + byte[] resultBytes = resultValue!; Assert.Equal(3, resultBytes[0]); // XOR of 65 (A) and 66 (B) is 3 } @@ -328,18 +328,18 @@ public async Task BitOperation_Not_PerformsCorrectOperation(BaseClient client) string keyPrefix = "{" + Guid.NewGuid().ToString() + "}"; string key1 = keyPrefix + ":key1"; string result = keyPrefix + ":result"; - + // Set key1 to "A" (01000001) await client.StringSetAsync(key1, "A"); - + // Perform NOT operation (NOT only takes one key) ValkeyKey[] keys = [key1]; long size = await client.StringBitOperationAsync(Bitwise.Not, result, keys); Assert.Equal(1, size); - + // Verify result: NOT A = NOT 01000001 = 10111110 = '¾' (ASCII 190) ValkeyValue resultValue = await client.StringGetAsync(result); - byte[] resultBytes = resultValue; + byte[] resultBytes = resultValue!; Assert.Equal(190, resultBytes[0]); // NOT of 65 (A) is 190 } @@ -348,10 +348,10 @@ public async Task BitOperation_Not_PerformsCorrectOperation(BaseClient client) public async Task BitField_GetSetIncrBy_WorksCorrectly(BaseClient client) { string key = Guid.NewGuid().ToString(); - + // Set initial value "A" (ASCII 65 = 01000001) await client.StringSetAsync(key, "A"); - + var subCommands = new BitFieldOptions.IBitFieldSubCommand[] { // Get 8 unsigned bits at offset 0 @@ -361,14 +361,14 @@ public async Task BitField_GetSetIncrBy_WorksCorrectly(BaseClient client) // Increment by 1 new BitFieldOptions.BitFieldIncrBy(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 1) }; - + long[] results = await client.StringBitFieldAsync(key, subCommands); - + Assert.Equal(3, results.Length); Assert.Equal(65, results[0]); // Original value 'A' Assert.Equal(65, results[1]); // Old value before SET Assert.Equal(67, results[2]); // New value after INCRBY (66 + 1 = 67 = 'C') - + // Verify final value ValkeyValue finalValue = await client.StringGetAsync(key); Assert.Equal("C", finalValue.ToString()); @@ -379,19 +379,19 @@ public async Task BitField_GetSetIncrBy_WorksCorrectly(BaseClient client) public async Task BitFieldReadOnly_Get_WorksCorrectly(BaseClient client) { string key = Guid.NewGuid().ToString(); - + // Set initial value "A" (ASCII 65 = 01000001) await client.StringSetAsync(key, "A"); - + var readOnlyCommands = new BitFieldOptions.IBitFieldReadOnlySubCommand[] { new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0)), new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(4), new BitFieldOptions.BitOffset(0)), new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(4), new BitFieldOptions.BitOffset(4)) }; - + long[] results = await client.StringBitFieldReadOnlyAsync(key, readOnlyCommands); - + Assert.Equal(3, results.Length); Assert.Equal(65, results[0]); // Full 8 bits: 01000001 = 65 Assert.Equal(4, results[1]); // First 4 bits: 0100 = 4 @@ -403,7 +403,7 @@ public async Task BitFieldReadOnly_Get_WorksCorrectly(BaseClient client) public async Task BitField_OverflowControl_WorksCorrectly(BaseClient client) { string key = Guid.NewGuid().ToString(); - + var subCommands = new BitFieldOptions.IBitFieldSubCommand[] { // Set overflow to WRAP first @@ -413,9 +413,9 @@ public async Task BitField_OverflowControl_WorksCorrectly(BaseClient client) // Increment u8 at offset 0 by 1 (255 + 1 = 0 with wrap) new BitFieldOptions.BitFieldIncrBy(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 1) }; - + long[] results = await client.StringBitFieldAsync(key, subCommands); - + Assert.Equal(2, results.Length); Assert.Equal(0, results[0]); // SET returns old value (0) Assert.Equal(0, results[1]); // 255 + 1 = 0 (wrapped) @@ -426,16 +426,16 @@ public async Task BitField_OverflowControl_WorksCorrectly(BaseClient client) public async Task BitField_OverflowSat_WorksCorrectly(BaseClient client) { string key = Guid.NewGuid().ToString(); - + var subCommands = new BitFieldOptions.IBitFieldSubCommand[] { new BitFieldOptions.BitFieldOverflow(BitFieldOptions.OverflowType.Sat), new BitFieldOptions.BitFieldSet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 250), new BitFieldOptions.BitFieldIncrBy(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 10) }; - + long[] results = await client.StringBitFieldAsync(key, subCommands); - + Assert.Equal(2, results.Length); Assert.Equal(0, results[0]); // SET returns old value (0) Assert.Equal(255, results[1]); // 250 + 10 = 255 (saturated at max) @@ -446,16 +446,16 @@ public async Task BitField_OverflowSat_WorksCorrectly(BaseClient client) public async Task BitField_OverflowFail_WorksCorrectly(BaseClient client) { string key = Guid.NewGuid().ToString(); - + var subCommands = new BitFieldOptions.IBitFieldSubCommand[] { new BitFieldOptions.BitFieldOverflow(BitFieldOptions.OverflowType.Fail), new BitFieldOptions.BitFieldSet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 255), new BitFieldOptions.BitFieldIncrBy(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 1) }; - + long[] results = await client.StringBitFieldAsync(key, subCommands); - + Assert.Equal(2, results.Length); Assert.Equal(0, results[0]); // SET returns old value (0) Assert.Equal(0, results[1]); // 255 + 1 = null (fail), converted to 0 @@ -466,7 +466,7 @@ public async Task BitField_OverflowFail_WorksCorrectly(BaseClient client) public async Task BitField_OffsetMultiplier_WorksCorrectly(BaseClient client) { string key = Guid.NewGuid().ToString(); - + var subCommands = new BitFieldOptions.IBitFieldSubCommand[] { // Set first i8 at offset #0 (0 * 8 = bit 0) @@ -477,13 +477,52 @@ public async Task BitField_OffsetMultiplier_WorksCorrectly(BaseClient client) new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Signed(8), new BitFieldOptions.BitOffsetMultiplier(0)), new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Signed(8), new BitFieldOptions.BitOffsetMultiplier(1)) }; - + long[] results = await client.StringBitFieldAsync(key, subCommands); - + Assert.Equal(4, results.Length); Assert.Equal(0, results[0]); // SET returns old value (0) Assert.Equal(0, results[1]); // SET returns old value (0) Assert.Equal(100, results[2]); // First i8 value Assert.Equal(-50, results[3]); // Second i8 value } -} \ No newline at end of file + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task BitField_AutoOptimization_UsesReadOnlyForGetOperations(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Set initial value "A" (ASCII 65 = 01000001) + await client.StringSetAsync(key, "A"); + + // Test with only GET operations - should automatically use BITFIELD_RO + var readOnlySubCommands = new BitFieldOptions.IBitFieldSubCommand[] + { + new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0)), + new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(4), new BitFieldOptions.BitOffset(0)), + new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(4), new BitFieldOptions.BitOffset(4)) + }; + + // This should internally use BITFIELD_RO since all commands are GET + long[] results = await client.StringBitFieldAsync(key, readOnlySubCommands); + + Assert.Equal(3, results.Length); + Assert.Equal(65, results[0]); // Full 8 bits: 01000001 = 65 + Assert.Equal(4, results[1]); // First 4 bits: 0100 = 4 + Assert.Equal(1, results[2]); // Next 4 bits: 0001 = 1 + + // Test with mixed operations - should use regular BITFIELD + var mixedSubCommands = new BitFieldOptions.IBitFieldSubCommand[] + { + new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0)), + new BitFieldOptions.BitFieldSet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 100) + }; + + long[] mixedResults = await client.StringBitFieldAsync(key, mixedSubCommands); + + Assert.Equal(2, mixedResults.Length); + Assert.Equal(65, mixedResults[0]); // Original value 'A' + Assert.Equal(65, mixedResults[1]); // Old value before SET + } +} diff --git a/tests/Valkey.Glide.UnitTests/CommandTests.cs b/tests/Valkey.Glide.UnitTests/CommandTests.cs index b0c3c3b4..a51a9415 100644 --- a/tests/Valkey.Glide.UnitTests/CommandTests.cs +++ b/tests/Valkey.Glide.UnitTests/CommandTests.cs @@ -314,7 +314,26 @@ public void ValidateCommandArgs() () => Assert.Equal(["PFCOUNT", "key"], Request.HyperLogLogLengthAsync("key").GetArgs()), () => Assert.Equal(["PFCOUNT", "key1", "key2", "key3"], Request.HyperLogLogLengthAsync(["key1", "key2", "key3"]).GetArgs()), () => Assert.Equal(["PFMERGE", "dest", "src1", "src2"], Request.HyperLogLogMergeAsync("dest", "src1", "src2").GetArgs()), - () => Assert.Equal(["PFMERGE", "dest", "src1", "src2", "src3"], Request.HyperLogLogMergeAsync("dest", ["src1", "src2", "src3"]).GetArgs()) + () => Assert.Equal(["PFMERGE", "dest", "src1", "src2", "src3"], Request.HyperLogLogMergeAsync("dest", ["src1", "src2", "src3"]).GetArgs()), + + // Bitmap Commands + () => Assert.Equal(["GETBIT", "key", "0"], Request.GetBitAsync("key", 0).GetArgs()), + () => Assert.Equal(["GETBIT", "key", "100"], Request.GetBitAsync("key", 100).GetArgs()), + () => Assert.Equal(["SETBIT", "key", "0", "1"], Request.SetBitAsync("key", 0, true).GetArgs()), + () => Assert.Equal(["SETBIT", "key", "5", "0"], Request.SetBitAsync("key", 5, false).GetArgs()), + () => Assert.Equal(["BITCOUNT", "key", "0", "-1"], Request.BitCountAsync("key", 0, -1, StringIndexType.Byte).GetArgs()), + () => Assert.Equal(["BITCOUNT", "key", "1", "5", "BIT"], Request.BitCountAsync("key", 1, 5, StringIndexType.Bit).GetArgs()), + () => Assert.Equal(["BITPOS", "key", "1", "0", "-1"], Request.BitPositionAsync("key", true, 0, -1, StringIndexType.Byte).GetArgs()), + () => Assert.Equal(["BITPOS", "key", "0", "2", "10", "BIT"], Request.BitPositionAsync("key", false, 2, 10, StringIndexType.Bit).GetArgs()), + () => Assert.Equal(["BITOP", "AND", "dest", "key1", "key2"], Request.BitOperationAsync(Bitwise.And, "dest", "key1", "key2").GetArgs()), + () => Assert.Equal(["BITOP", "OR", "dest", "key1", "key2", "key3"], Request.BitOperationAsync(Bitwise.Or, "dest", ["key1", "key2", "key3"]).GetArgs()), + () => Assert.Equal(["BITOP", "XOR", "dest", "key1", "key2"], Request.BitOperationAsync(Bitwise.Xor, "dest", ["key1", "key2"]).GetArgs()), + () => Assert.Equal(["BITOP", "NOT", "dest", "key1"], Request.BitOperationAsync(Bitwise.Not, "dest", ["key1"]).GetArgs()), + () => Assert.Equal(["BITFIELD", "key", "GET", "u8", "0"], Request.BitFieldAsync("key", [new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0))]).GetArgs()), + () => Assert.Equal(["BITFIELD", "key", "SET", "i16", "#1", "100"], Request.BitFieldAsync("key", [new BitFieldOptions.BitFieldSet(BitFieldOptions.Encoding.Signed(16), new BitFieldOptions.BitOffsetMultiplier(1), 100)]).GetArgs()), + () => Assert.Equal(["BITFIELD", "key", "INCRBY", "u32", "8", "5"], Request.BitFieldAsync("key", [new BitFieldOptions.BitFieldIncrBy(BitFieldOptions.Encoding.Unsigned(32), new BitFieldOptions.BitOffset(8), 5)]).GetArgs()), + () => Assert.Equal(["BITFIELD", "key", "OVERFLOW", "WRAP", "SET", "u8", "0", "255"], Request.BitFieldAsync("key", [new BitFieldOptions.BitFieldOverflow(BitFieldOptions.OverflowType.Wrap), new BitFieldOptions.BitFieldSet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 255)]).GetArgs()), + () => Assert.Equal(["BITFIELDREADONLY", "key", "GET", "u8", "0", "GET", "i4", "8"], Request.BitFieldReadOnlyAsync("key", [new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0)), new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Signed(4), new BitFieldOptions.BitOffset(8))]).GetArgs()) ); } @@ -577,7 +596,21 @@ public void ValidateCommandConverters() () => Assert.Equal(0L, Request.HyperLogLogLengthAsync("key").Converter(0L)), () => Assert.Equal(100L, Request.HyperLogLogLengthAsync(["key1", "key2"]).Converter(100L)), () => Assert.Equal("OK", Request.HyperLogLogMergeAsync("dest", "src1", "src2").Converter("OK")), - () => Assert.Equal("OK", Request.HyperLogLogMergeAsync("dest", ["src1", "src2"]).Converter("OK")) + () => Assert.Equal("OK", Request.HyperLogLogMergeAsync("dest", ["src1", "src2"]).Converter("OK")), + + // Bitmap Command Converters + () => Assert.True(Request.GetBitAsync("key", 0).Converter(1L)), + () => Assert.False(Request.GetBitAsync("key", 0).Converter(0L)), + () => Assert.True(Request.SetBitAsync("key", 0, true).Converter(1L)), + () => Assert.False(Request.SetBitAsync("key", 0, false).Converter(0L)), + () => Assert.Equal(26L, Request.BitCountAsync("key", 0, -1, StringIndexType.Byte).Converter(26L)), + () => Assert.Equal(0L, Request.BitCountAsync("key", 0, -1, StringIndexType.Byte).Converter(0L)), + () => Assert.Equal(2L, Request.BitPositionAsync("key", true, 0, -1, StringIndexType.Byte).Converter(2L)), + () => Assert.Equal(-1L, Request.BitPositionAsync("key", true, 0, -1, StringIndexType.Byte).Converter(-1L)), + () => Assert.Equal(6L, Request.BitOperationAsync(Bitwise.And, "dest", "key1", "key2").Converter(6L)), + () => Assert.Equal(0L, Request.BitOperationAsync(Bitwise.Or, "dest", ["key1", "key2"]).Converter(0L)), + () => Assert.Equal([65L, 0L, 100L], Request.BitFieldAsync("key", [new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0)), new BitFieldOptions.BitFieldSet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 100)]).Converter([65L, null!, 100L])), + () => Assert.Equal([65L, 4L], Request.BitFieldReadOnlyAsync("key", [new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0)), new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(4), new BitFieldOptions.BitOffset(0))]).Converter([65L, 4L])) ); } From 872b1d874b84ec3ec3aa09bc19dfd74168edd810 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Mon, 3 Nov 2025 15:56:42 -0800 Subject: [PATCH 07/13] Update version in readme Signed-off-by: Alex Rehnby-Martin --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 923979d9..99b5b5e6 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,10 @@ Valkey General Language Independent Driver for the Enterprise (GLIDE) is the off Valkey GLIDE for C# is API-compatible with the following engine versions: -| Engine Type | 6.2 | 7.0 | 7.1 | 7.2 | 8.0 | 8.1 | -|-----------------------|-------|-------|--------|-------|-------|-------| -| Valkey | - | - | - | V | V | V | -| Redis | V | V | V | V | - | - | +| Engine Type | 6.2 | 7.0 | 7.1 | 7.2 | 8.0 | 8.1 | 9.0 | +|-----------------------|-------|-------|--------|-------|-------|-------|-------| +| Valkey | - | - | - | V | V | V | V | +| Redis | V | V | V | V | - | - | - | ## Installation From 8cb17a1196113740dab32675b08dfd515cc2497d Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Mon, 3 Nov 2025 16:31:01 -0800 Subject: [PATCH 08/13] Format Signed-off-by: Alex Rehnby-Martin --- tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs | 2 -- tests/Valkey.Glide.UnitTests/CommandTests.cs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs b/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs index 8329f3a2..0ce5a19a 100644 --- a/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs +++ b/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs @@ -2,8 +2,6 @@ using Valkey.Glide.Commands.Options; -using Valkey.Glide.Commands.Options; - namespace Valkey.Glide.IntegrationTests; internal class BatchTestUtils diff --git a/tests/Valkey.Glide.UnitTests/CommandTests.cs b/tests/Valkey.Glide.UnitTests/CommandTests.cs index 8a4d5227..ca4d7800 100644 --- a/tests/Valkey.Glide.UnitTests/CommandTests.cs +++ b/tests/Valkey.Glide.UnitTests/CommandTests.cs @@ -335,7 +335,7 @@ public void ValidateCommandArgs() () => Assert.Equal(["BITFIELD", "key", "INCRBY", "u32", "8", "5"], Request.BitFieldAsync("key", [new BitFieldOptions.BitFieldIncrBy(BitFieldOptions.Encoding.Unsigned(32), new BitFieldOptions.BitOffset(8), 5)]).GetArgs()), () => Assert.Equal(["BITFIELD", "key", "OVERFLOW", "WRAP", "SET", "u8", "0", "255"], Request.BitFieldAsync("key", [new BitFieldOptions.BitFieldOverflow(BitFieldOptions.OverflowType.Wrap), new BitFieldOptions.BitFieldSet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 255)]).GetArgs()), () => Assert.Equal(["BITFIELDREADONLY", "key", "GET", "u8", "0", "GET", "i4", "8"], Request.BitFieldReadOnlyAsync("key", [new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0)), new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Signed(4), new BitFieldOptions.BitOffset(8))]).GetArgs()), - + // Hash Field Expire Commands (Valkey 9.0+) () => Assert.Equal(["HGETEX", "key", "EX", "60", "FIELDS", "2", "field1", "field2"], Request.HashGetExAsync("key", ["field1", "field2"], new HashGetExOptions().SetExpiry(HGetExExpiry.Seconds(60))).GetArgs()), () => Assert.Equal(["HGETEX", "key", "PX", "5000", "FIELDS", "1", "field1"], Request.HashGetExAsync("key", ["field1"], new HashGetExOptions().SetExpiry(HGetExExpiry.Milliseconds(5000))).GetArgs()), From 56f47c0de862ea7b58b48774060e364eecf515f5 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Wed, 5 Nov 2025 08:53:43 -0800 Subject: [PATCH 09/13] Attempt to set higher timeout for tests Signed-off-by: Alex Rehnby-Martin --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d99d2559..3c86baa8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -82,7 +82,7 @@ jobs: tests: name: net${{ matrix.dotnet }}, server ${{ matrix.server.version }}, ${{ matrix.host.TARGET }} needs: get-matrices - timeout-minutes: 100 + timeout-minutes: 200 strategy: fail-fast: false matrix: From 6b0cb34aa67f60ec3c27fa3ae5ec3988373a8b62 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Thu, 6 Nov 2025 14:51:41 -0800 Subject: [PATCH 10/13] Add skip for tests of functionality only supported in server versions 7.0.0+ Signed-off-by: Alex Rehnby-Martin --- .../BitmapCommandTests.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs index 1ddaea0c..d2ecef5f 100644 --- a/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs @@ -165,6 +165,11 @@ public async Task BitCount_NonExistentKey_ReturnsZero(BaseClient client) [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task BitCount_WithBitIndex_CountsCorrectly(BaseClient client) { + Assert.SkipWhen( + TestConfiguration.SERVER_VERSION < new Version("7.0.0"), + "BIT index type for BITCOUNT requires server version 7.0 or higher" + ); + string key = Guid.NewGuid().ToString(); // Set multiple bits @@ -214,6 +219,11 @@ public async Task BitPosition_NonExistentKey_ReturnsMinusOne(BaseClient client) [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task BitPosition_WithRange_FindsInRange(BaseClient client) { + Assert.SkipWhen( + TestConfiguration.SERVER_VERSION < new Version("7.0.0"), + "BIT index type for BITPOS requires server version 7.0 or higher" + ); + string key = Guid.NewGuid().ToString(); // Set multiple bits: bit 1 and bit 9 From 560a37e00b3c28a201cb3aca779bdeaaa99b48c0 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Fri, 7 Nov 2025 08:06:17 -0800 Subject: [PATCH 11/13] Update sources/Valkey.Glide/Commands/IBitmapCommands.cs Co-authored-by: Taylor Curran Signed-off-by: Alex Rehnby-Martin --- sources/Valkey.Glide/Commands/IBitmapCommands.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/Valkey.Glide/Commands/IBitmapCommands.cs b/sources/Valkey.Glide/Commands/IBitmapCommands.cs index b089b478..c6d96982 100644 --- a/sources/Valkey.Glide/Commands/IBitmapCommands.cs +++ b/sources/Valkey.Glide/Commands/IBitmapCommands.cs @@ -3,7 +3,7 @@ namespace Valkey.Glide.Commands; /// -/// Supports commands for the "Bitmap Commands" group for standalone and cluster clients. +/// Supports bitmap commands for standalone and cluster clients. ///
/// See more on valkey.io. ///
From 0b665165a40af53dbfa1ddcde7fa7e9283bd4d03 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Fri, 7 Nov 2025 08:09:41 -0800 Subject: [PATCH 12/13] Update tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs Co-authored-by: Taylor Curran Signed-off-by: Alex Rehnby-Martin --- .../BitmapCommandTests.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs index d2ecef5f..d93fdd1d 100644 --- a/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs @@ -18,17 +18,11 @@ public async Task GetBit_ReturnsCorrectBitValue(BaseClient client) await client.StringSetAsync(key, "A"); // Test bit positions in 'A' (01000001) - bool bit0 = await client.StringGetBitAsync(key, 0); // Should be false (0) - bool bit1 = await client.StringGetBitAsync(key, 1); // Should be true (1) - bool bit2 = await client.StringGetBitAsync(key, 2); // Should be false (0) - bool bit6 = await client.StringGetBitAsync(key, 6); // Should be false (0) - bool bit7 = await client.StringGetBitAsync(key, 7); // Should be true (1) - - Assert.False(bit0); - Assert.True(bit1); - Assert.False(bit2); - Assert.False(bit6); - Assert.True(bit7); + Assert.False(await client.StringGetBitAsync(key, 0)); + Assert.True(await client.StringGetBitAsync(key, 1)); + Assert.False(await client.StringGetBitAsync(key, 2)); + Assert.False(await client.StringGetBitAsync(key, 6)); + Assert.True(await client.StringGetBitAsync(key, 7)); } [Theory(DisableDiscoveryEnumeration = true)] From ab09d51afb5d2d4a3223f2ac16f509ba73d4a0a5 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Fri, 7 Nov 2025 12:12:24 -0800 Subject: [PATCH 13/13] Resolve PR feedback Signed-off-by: Alex Rehnby-Martin --- .github/workflows/tests.yml | 2 +- .../Valkey.Glide/Commands/IBitmapCommands.cs | 6 ++-- sources/Valkey.Glide/GlideString.cs | 7 ++++ .../Internals/Request.BitmapCommands.cs | 8 ++--- .../BitmapCommandTests.cs | 32 ++++++++++++++++--- tests/Valkey.Glide.UnitTests/CommandTests.cs | 26 +++++++++++++++ 6 files changed, 69 insertions(+), 12 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3c86baa8..d99d2559 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -82,7 +82,7 @@ jobs: tests: name: net${{ matrix.dotnet }}, server ${{ matrix.server.version }}, ${{ matrix.host.TARGET }} needs: get-matrices - timeout-minutes: 200 + timeout-minutes: 100 strategy: fail-fast: false matrix: diff --git a/sources/Valkey.Glide/Commands/IBitmapCommands.cs b/sources/Valkey.Glide/Commands/IBitmapCommands.cs index c6d96982..45829be9 100644 --- a/sources/Valkey.Glide/Commands/IBitmapCommands.cs +++ b/sources/Valkey.Glide/Commands/IBitmapCommands.cs @@ -36,7 +36,7 @@ public interface IBitmapCommands /// The offset in the string to set the bit at. /// The bit value to set (true for 1, false for 0). /// The flags to use for this operation. Currently flags are ignored. - /// The original bit value stored at offset. + /// The original bit value stored at offset. Returns false if the key does not exist or if the offset is beyond the string length. /// /// /// @@ -141,7 +141,7 @@ public interface IBitmapCommands /// The key of the string. /// The subcommands to execute (GET, SET, INCRBY). /// The flags to use for this operation. Currently flags are ignored. - /// An array of results from the executed subcommands. + /// An array of results from the executed subcommands. Null responses from the server are converted to 0. /// /// /// @@ -166,7 +166,7 @@ public interface IBitmapCommands /// The key of the string. /// The GET subcommands to execute. /// The flags to use for this operation. Currently flags are ignored. - /// An array of results from the executed GET subcommands. + /// An array of results from the executed GET subcommands. Null responses from the server are converted to 0. /// /// /// diff --git a/sources/Valkey.Glide/GlideString.cs b/sources/Valkey.Glide/GlideString.cs index 37f47738..ff2c72c0 100644 --- a/sources/Valkey.Glide/GlideString.cs +++ b/sources/Valkey.Glide/GlideString.cs @@ -45,6 +45,13 @@ public static GlideString ToGlideString(this double @double) : double.IsNaN(@double) ? new("nan") : new(@double.ToString("G17", System.Globalization.CultureInfo.InvariantCulture)); + /// + /// Convert a to a ("1" for true, "0" for false). + /// + /// A to convert. + /// A . + public static GlideString ToGlideString(this bool @bool) => new(@bool ? "1" : "0"); + /// /// Convert a to a . /// diff --git a/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs b/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs index f34b73c7..b9c089f2 100644 --- a/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs +++ b/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs @@ -12,9 +12,9 @@ public static Cmd GetBitAsync(ValkeyKey key, long offset) => new(RequestType.GetBit, [key.ToGlideString(), offset.ToGlideString()], false, response => response != 0); public static Cmd SetBitAsync(ValkeyKey key, long offset, bool value) - => new(RequestType.SetBit, [key.ToGlideString(), offset.ToGlideString(), (value ? 1 : 0).ToGlideString()], false, response => response != 0); + => new(RequestType.SetBit, [key.ToGlideString(), offset.ToGlideString(), value.ToGlideString()], false, response => response != 0); - public static Cmd BitCountAsync(ValkeyKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte) + public static Cmd BitCountAsync(ValkeyKey key, long start, long end, StringIndexType indexType) { List args = [key.ToGlideString(), start.ToGlideString(), end.ToGlideString()]; if (indexType != StringIndexType.Byte) @@ -24,9 +24,9 @@ public static Cmd BitCountAsync(ValkeyKey key, long start = 0, long return Simple(RequestType.BitCount, [.. args]); } - public static Cmd BitPositionAsync(ValkeyKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte) + public static Cmd BitPositionAsync(ValkeyKey key, bool bit, long start, long end, StringIndexType indexType) { - List args = [key.ToGlideString(), (bit ? 1 : 0).ToGlideString(), start.ToGlideString(), end.ToGlideString()]; + List args = [key.ToGlideString(), bit.ToGlideString(), start.ToGlideString(), end.ToGlideString()]; if (indexType != StringIndexType.Byte) { args.Add(indexType.ToLiteral().ToGlideString()); diff --git a/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs index d93fdd1d..8f8bbb5b 100644 --- a/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs +++ b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs @@ -2,6 +2,8 @@ using Valkey.Glide.Commands.Options; +using static Valkey.Glide.Errors; + namespace Valkey.Glide.IntegrationTests; public class BitmapCommandTests(TestConfiguration config) @@ -52,6 +54,19 @@ public async Task GetBit_OffsetBeyondString_ReturnsFalse(BaseClient client) Assert.False(bit); } + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task GetBit_NegativeOffset_ThrowsException(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Set a string + await client.StringSetAsync(key, "A"); + + // Test negative offset - should throw an exception + await Assert.ThrowsAsync(async () => await client.StringGetBitAsync(key, -1)); + } + [Theory(DisableDiscoveryEnumeration = true)] [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task SetBit_SetsAndReturnsOriginalValue(BaseClient client) @@ -90,6 +105,16 @@ public async Task SetBit_NonExistentKey_CreatesKey(BaseClient client) Assert.True(currentBit); } + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + public async Task SetBit_NegativeOffset_ThrowsException(BaseClient client) + { + string key = Guid.NewGuid().ToString(); + + // Test negative offset - should throw an exception + await Assert.ThrowsAsync(async () => await client.StringSetBitAsync(key, -1, true)); + } + [Theory(DisableDiscoveryEnumeration = true)] [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] public async Task GetSetBit_CombinedOperations_WorksTogether(BaseClient client) @@ -493,14 +518,14 @@ public async Task BitField_OffsetMultiplier_WorksCorrectly(BaseClient client) [Theory(DisableDiscoveryEnumeration = true)] [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] - public async Task BitField_AutoOptimization_UsesReadOnlyForGetOperations(BaseClient client) + public async Task BitField_WithReadOnlyAndMixedOperations_WorksCorrectly(BaseClient client) { string key = Guid.NewGuid().ToString(); // Set initial value "A" (ASCII 65 = 01000001) await client.StringSetAsync(key, "A"); - // Test with only GET operations - should automatically use BITFIELD_RO + // Test with only GET operations var readOnlySubCommands = new BitFieldOptions.IBitFieldSubCommand[] { new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0)), @@ -508,7 +533,6 @@ public async Task BitField_AutoOptimization_UsesReadOnlyForGetOperations(BaseCli new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(4), new BitFieldOptions.BitOffset(4)) }; - // This should internally use BITFIELD_RO since all commands are GET long[] results = await client.StringBitFieldAsync(key, readOnlySubCommands); Assert.Equal(3, results.Length); @@ -516,7 +540,7 @@ public async Task BitField_AutoOptimization_UsesReadOnlyForGetOperations(BaseCli Assert.Equal(4, results[1]); // First 4 bits: 0100 = 4 Assert.Equal(1, results[2]); // Next 4 bits: 0001 = 1 - // Test with mixed operations - should use regular BITFIELD + // Test with mixed operations var mixedSubCommands = new BitFieldOptions.IBitFieldSubCommand[] { new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0)), diff --git a/tests/Valkey.Glide.UnitTests/CommandTests.cs b/tests/Valkey.Glide.UnitTests/CommandTests.cs index ca4d7800..8b55d96e 100644 --- a/tests/Valkey.Glide.UnitTests/CommandTests.cs +++ b/tests/Valkey.Glide.UnitTests/CommandTests.cs @@ -660,6 +660,32 @@ public void ValidateCommandConverters() ); } + [Fact] + public void BitField_AutoOptimization_UsesCorrectRequestType() + { + // Test that read-only subcommands use BitFieldReadOnlyAsync + var readOnlySubCommands = new BitFieldOptions.IBitFieldSubCommand[] + { + new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0)), + new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(4), new BitFieldOptions.BitOffset(0)) + }; + + // Verify that all subcommands are read-only + bool allReadOnly = readOnlySubCommands.All(cmd => cmd is BitFieldOptions.IBitFieldReadOnlySubCommand); + Assert.True(allReadOnly); + + // Test that mixed subcommands don't qualify for read-only optimization + var mixedSubCommands = new BitFieldOptions.IBitFieldSubCommand[] + { + new BitFieldOptions.BitFieldGet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0)), + new BitFieldOptions.BitFieldSet(BitFieldOptions.Encoding.Unsigned(8), new BitFieldOptions.BitOffset(0), 100) + }; + + // Verify that mixed subcommands are not all read-only + bool mixedAllReadOnly = mixedSubCommands.All(cmd => cmd is BitFieldOptions.IBitFieldReadOnlySubCommand); + Assert.False(mixedAllReadOnly); + } + [Fact] public void ValidateStringCommandArrayConverters() {