diff --git a/README.md b/README.md index 9211605..3531134 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 diff --git a/sources/Valkey.Glide/BaseClient.BitmapCommands.cs b/sources/Valkey.Glide/BaseClient.BitmapCommands.cs new file mode 100644 index 0000000..aa9beec --- /dev/null +++ b/sources/Valkey.Glide/BaseClient.BitmapCommands.cs @@ -0,0 +1,77 @@ +// 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; + +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)); + } + + /// + 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)); + } + + /// + 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)); + } + + /// + 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)); + } + + /// + 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)); + } + + /// + 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)); + } +} diff --git a/sources/Valkey.Glide/Commands/IBitmapCommands.cs b/sources/Valkey.Glide/Commands/IBitmapCommands.cs new file mode 100644 index 0000000..45829be --- /dev/null +++ b/sources/Valkey.Glide/Commands/IBitmapCommands.cs @@ -0,0 +1,183 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +namespace Valkey.Glide.Commands; + +/// +/// Supports bitmap commands 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); + + /// + /// 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. Returns false if the key does not exist or if the offset is beyond the string length. + /// + /// + /// + /// 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); + + /// + /// 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); + + /// + /// 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); + + /// + /// 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. Null responses from the server are converted to 0. + /// + /// + /// + /// 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. Null responses from the server are converted to 0. + /// + /// + /// + /// 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); +} diff --git a/sources/Valkey.Glide/Commands/Options/BitFieldOptions.cs b/sources/Valkey.Glide/Commands/Options/BitFieldOptions.cs new file mode 100644 index 0000000..1f63842 --- /dev/null +++ b/sources/Valkey.Glide/Commands/Options/BitFieldOptions.cs @@ -0,0 +1,136 @@ +// 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. + /// + /// The bit offset value. + public class BitOffset(long offset) : IBitOffset + { + public string GetOffset() => offset.ToString(); + } + + /// + /// Offset multiplied by encoding width (prefixed with #). + /// + /// The multiplier value. + public class BitOffsetMultiplier(long multiplier) : IBitOffset + { + public string GetOffset() => $"#{multiplier}"; + } + + /// + /// GET subcommand for reading bits from the string. + /// + /// The bit field encoding. + /// The bit field offset. + public class BitFieldGet(string encoding, IBitOffset offset) : IBitFieldReadOnlySubCommand + { + public string[] ToArgs() => ["GET", encoding, offset.GetOffset()]; + } + + /// + /// SET subcommand for setting bits in the string. + /// + /// The bit field encoding. + /// The bit field offset. + /// The value to set. + public class BitFieldSet(string encoding, IBitOffset offset, long value) : IBitFieldSubCommand + { + public string[] ToArgs() => ["SET", encoding, offset.GetOffset(), value.ToString()]; + } + + /// + /// INCRBY subcommand for incrementing bits in the string. + /// + /// The bit field encoding. + /// The bit field offset. + /// The increment value. + public class BitFieldIncrBy(string encoding, IBitOffset offset, long increment) : IBitFieldSubCommand + { + public string[] ToArgs() => ["INCRBY", encoding, offset.GetOffset(), increment.ToString()]; + } + + /// + /// OVERFLOW subcommand for controlling overflow behavior. + /// + /// The overflow behavior type. + public class BitFieldOverflow(OverflowType overflowType) : IBitFieldSubCommand + { + 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}"; + } + + +} diff --git a/sources/Valkey.Glide/GlideString.cs b/sources/Valkey.Glide/GlideString.cs index 37f4773..ff2c72c 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 new file mode 100644 index 0000000..b9c089f --- /dev/null +++ b/sources/Valkey.Glide/Internals/Request.BitmapCommands.cs @@ -0,0 +1,71 @@ +// 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; + +internal partial class Request +{ + 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.ToGlideString()], false, response => response != 0); + + public static Cmd BitCountAsync(ValkeyKey key, long start, long end, StringIndexType indexType) + { + List args = [key.ToGlideString(), start.ToGlideString(), end.ToGlideString()]; + if (indexType != StringIndexType.Byte) + { + args.Add(indexType.ToLiteral().ToGlideString()); + } + return Simple(RequestType.BitCount, [.. args]); + } + + public static Cmd BitPositionAsync(ValkeyKey key, bool bit, long start, long end, StringIndexType indexType) + { + List args = [key.ToGlideString(), bit.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]); + } + + public static Cmd BitFieldAsync(ValkeyKey key, BitFieldOptions.IBitFieldSubCommand[] subCommands) + { + List args = [key.ToGlideString()]; + foreach (var subCommand in subCommands) + { + args.AddRange(subCommand.ToArgs().ToGlideStrings()); + } + 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) + { + List args = [key.ToGlideString()]; + foreach (var subCommand in subCommands) + { + args.AddRange(subCommand.ToArgs().ToGlideStrings()); + } + return new(RequestType.BitFieldReadOnly, [.. args], false, response => + [.. response.Select(item => item is null ? 0L : Convert.ToInt64(item))]); + } +} diff --git a/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs b/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs new file mode 100644 index 0000000..0cdda6e --- /dev/null +++ b/sources/Valkey.Glide/Pipeline/BaseBatch.BitmapCommands.cs @@ -0,0 +1,42 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +using Valkey.Glide.Commands.Options; +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)); + + /// + 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)); + + /// + 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)); + + /// + 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); +} diff --git a/sources/Valkey.Glide/Pipeline/IBatch.cs b/sources/Valkey.Glide/Pipeline/IBatch.cs index 6195a96..16b4f2e 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, IBatchGeospatialCommands, IBatchHyperLogLogCommands +internal interface IBatch : IBatchSetCommands, IBatchStringCommands, IBatchListCommands, IBatchSortedSetCommands, IBatchGenericCommands, IBatchConnectionManagementCommands, IBatchHashCommands, IBatchServerManagementCommands, IBatchGeospatialCommands, 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 0000000..1a4f9db --- /dev/null +++ b/sources/Valkey.Glide/Pipeline/IBatchBitmapCommands.cs @@ -0,0 +1,41 @@ +// 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); + + /// + /// 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); + + /// + /// 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); + + /// + /// Command Response - + IBatch StringBitField(ValkeyKey key, Commands.Options.BitFieldOptions.IBitFieldSubCommand[] subCommands); + + /// + /// Command Response - + IBatch StringBitFieldReadOnly(ValkeyKey key, Commands.Options.BitFieldOptions.IBitFieldReadOnlySubCommand[] subCommands); +} diff --git a/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs b/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs index f955a6a..0ce5a19 100644 --- a/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs +++ b/tests/Valkey.Glide.IntegrationTests/BatchTestUtils.cs @@ -1,7 +1,5 @@ // Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 - - using Valkey.Glide.Commands.Options; namespace Valkey.Glide.IntegrationTests; @@ -1504,6 +1502,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[] { @@ -1514,6 +1600,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 new file mode 100644 index 0000000..8f8bbb5 --- /dev/null +++ b/tests/Valkey.Glide.IntegrationTests/BitmapCommandTests.cs @@ -0,0 +1,556 @@ +// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +using Valkey.Glide.Commands.Options; + +using static Valkey.Glide.Errors; + +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) + 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)] + [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); + } + + [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) + { + 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 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) + { + 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); + } + + [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) + { + 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 + 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); + } + + [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) + { + 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 + 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 + } + + [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 + } + + [Theory(DisableDiscoveryEnumeration = true)] + [MemberData(nameof(Config.TestClients), MemberType = typeof(TestConfiguration))] + 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 + 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)) + }; + + 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 + 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 30fee0e..8b55d96 100644 --- a/tests/Valkey.Glide.UnitTests/CommandTests.cs +++ b/tests/Valkey.Glide.UnitTests/CommandTests.cs @@ -317,6 +317,25 @@ public void ValidateCommandArgs() () => 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()), + // 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()), + // 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()), @@ -623,10 +642,50 @@ 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])) ); } + [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() {