diff --git a/src/StackExchange.Redis/APITypes/BitfieldOperation.cs b/src/StackExchange.Redis/APITypes/BitfieldOperation.cs new file mode 100644 index 000000000..1802ceaa4 --- /dev/null +++ b/src/StackExchange.Redis/APITypes/BitfieldOperation.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StackExchange.Redis; + +/// +/// Represents a single Bitfield Operation. +/// +public struct BitfieldOperation +{ + private static string CreateOffset(bool offsetByBit, long offset) => $"{(offsetByBit ? string.Empty : "#")}{offset}"; + private static readonly string[] Encodings = Enumerable.Range(0, 127).Select(x => // 0? + { + var size = x % 64; + var signedness = x < 65 ? "i" : "u"; + return $"{signedness}{size}"; + }).ToArray(); + + private static string CreateEncoding(bool unsigned, byte size) + { + if (size == 0) + { + throw new ArgumentException("Invalid encoding, size must be non-zero", nameof(size)); + } + + if (unsigned && size > 63) + { + throw new ArgumentException( + $"Invalid Encoding, unsigned bitfield operations support a maximum size of 63, provided size: {size}", nameof(size)); + } + + if (size > 64) + { + throw new ArgumentException( + $"Invalid Encoding, signed bitfield operations support a maximum size of 64, provided size: {size}", nameof(size)); + } + + return Encodings[size + (!unsigned ? 0 : 64)]; + } + + internal string Offset; + internal long? Value; + internal BitFieldSubCommand SubCommand; + internal string Encoding; + internal BitfieldOverflowHandling? BitfieldOverflowHandling; + + /// + /// Creates a Get Bitfield Subcommand struct to retrieve a single integer from the bitfield. + /// + /// The offset into the bitfield to address. + /// The width of the encoding to interpret the bitfield width. + /// Whether or not to offset into the bitfield by bits vs encoding. + /// Whether or not to interpret the number gotten as an unsigned integer. + /// + public static BitfieldOperation Get(long offset, byte width, bool offsetByBit = true, bool unsigned = false) + { + var offsetValue = CreateOffset(offsetByBit, offset); + return new BitfieldOperation + { + Offset = offsetValue, + Value = null, + SubCommand = BitFieldSubCommand.Get, + Encoding = CreateEncoding(unsigned, width) + }; + } + + /// + /// Creates a Set Bitfield SubCommand to set a single integer from the bitfield. + /// + /// The offset into the bitfield to address. + /// The width of the encoding to interpret the bitfield width. + /// The value to set the addressed bits to. + /// Whether or not to offset into the bitfield by bits vs encoding. + /// Whether or not to interpret the number gotten as an unsigned integer. + /// + public static BitfieldOperation Set(long offset, byte width, long value, bool offsetByBit = true, bool unsigned = false) + { + var offsetValue = CreateOffset(offsetByBit, offset); + return new BitfieldOperation + { + Offset = offsetValue, + Value = value, + SubCommand = BitFieldSubCommand.Set, + Encoding = CreateEncoding(unsigned, width) + }; + } + + /// + /// Creates an Increment Bitfield SubCommand to increment a single integer from the bitfield. + /// + /// The offset into the bitfield to address. + /// The width of the encoding to interpret the bitfield width. + /// The value to set the addressed bits to. + /// Whether or not to offset into the bitfield by bits vs encoding. + /// Whether or not to interpret the number gotten as an unsigned integer. + /// How to handle overflows. + /// + public static BitfieldOperation Increment(long offset, byte width, long increment, bool offsetByBit = true, bool unsigned = false, BitfieldOverflowHandling overflowHandling = Redis.BitfieldOverflowHandling.Wrap) + { + var offsetValue = CreateOffset(offsetByBit, offset); + return new BitfieldOperation + { + Offset = offsetValue, + Value = increment, + SubCommand = BitFieldSubCommand.Increment, + Encoding = CreateEncoding(unsigned, width), + BitfieldOverflowHandling = overflowHandling + }; + } + + internal IEnumerable EnumerateArgs() + { + if (SubCommand != BitFieldSubCommand.Get) + { + if (BitfieldOverflowHandling is not null && BitfieldOverflowHandling != Redis.BitfieldOverflowHandling.Wrap) + { + yield return RedisLiterals.OVERFLOW; + yield return BitfieldOverflowHandling.Value.AsRedisValue(); + } + } + + yield return SubCommand.AsRedisValue(); + yield return Encoding; + yield return Offset; + if (SubCommand != BitFieldSubCommand.Get) + { + if (Value is null) + { + throw new ArgumentNullException($"Value must not be null for {SubCommand.AsRedisValue()} commands"); + } + + yield return Value; + } + } + + internal int NumArgs() + { + var numArgs = 3; + if (SubCommand != BitFieldSubCommand.Get) + { + numArgs += BitfieldOverflowHandling is not null && BitfieldOverflowHandling != Redis.BitfieldOverflowHandling.Wrap ? 3 : 1; + } + + return numArgs; + } +} + +internal static class BitfieldOperationExtensions +{ + internal static BitfieldCommandMessage BuildMessage(this BitfieldOperation[] subCommands, int db, RedisKey key, + CommandFlags flags, RedisBase redisBase, out ServerEndPoint? server) + { + var eligibleForReadOnly = subCommands.All(x => x.SubCommand == BitFieldSubCommand.Get); + var features = redisBase.GetFeatures(key, flags, eligibleForReadOnly ? RedisCommand.BITFIELD_RO : RedisCommand.BITFIELD, out server); + var command = eligibleForReadOnly && features.ReadOnlyBitfield ? RedisCommand.BITFIELD_RO : RedisCommand.BITFIELD; + return new BitfieldCommandMessage(db, flags, key, command, subCommands.SelectMany(x=>x.EnumerateArgs()).ToArray()); + } + + internal static BitfieldCommandMessage BuildMessage(this BitfieldOperation subCommand, int db, RedisKey key, + CommandFlags flags, RedisBase redisBase, out ServerEndPoint? server) + { + var eligibleForReadOnly = subCommand.SubCommand == BitFieldSubCommand.Get; + var features = redisBase.GetFeatures(key, flags, eligibleForReadOnly ? RedisCommand.BITFIELD_RO : RedisCommand.BITFIELD, out server); + var command = eligibleForReadOnly && features.ReadOnlyBitfield ? RedisCommand.BITFIELD_RO : RedisCommand.BITFIELD; + return new BitfieldCommandMessage(db, flags, key, command, subCommand.EnumerateArgs().ToArray()); + } +} + +/// +/// Bitfield subcommands. +/// +public enum BitFieldSubCommand +{ + /// + /// Subcommand to get the bitfield value. + /// + Get, + + /// + /// Subcommand to set the bitfield value. + /// + Set, + + /// + /// Subcommand to increment the bitfield value + /// + Increment +} + +internal static class BitfieldSubCommandExtensions +{ + internal static RedisValue AsRedisValue(this BitFieldSubCommand subCommand) => + subCommand switch + { + BitFieldSubCommand.Get => RedisLiterals.GET, + BitFieldSubCommand.Set => RedisLiterals.SET, + BitFieldSubCommand.Increment => RedisLiterals.INCRBY, + _ => throw new ArgumentOutOfRangeException(nameof(subCommand)) + }; +} + +internal class BitfieldCommandMessage : Message +{ + private readonly IEnumerable _args; + private readonly RedisKey _key; + public BitfieldCommandMessage(int db, CommandFlags flags, RedisKey key, RedisCommand command, RedisValue[] args) : base(db, flags, command) + { + _key = key; + _args = args; + } + + public override int ArgCount => 1 + _args.Count(); + + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, ArgCount); + physical.Write(_key); + foreach (var arg in _args) + { + physical.WriteBulkString(arg); + } + } +} diff --git a/src/StackExchange.Redis/Enums/BitfieldOverflowHandling.cs b/src/StackExchange.Redis/Enums/BitfieldOverflowHandling.cs new file mode 100644 index 000000000..df9275f34 --- /dev/null +++ b/src/StackExchange.Redis/Enums/BitfieldOverflowHandling.cs @@ -0,0 +1,33 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// Defines the overflow behavior of a BITFIELD command. +/// +public enum BitfieldOverflowHandling +{ + /// + /// Wraps around to the most negative value of signed integers, or zero for unsigned integers + /// + Wrap, + /// + /// Uses saturation arithmetic, stopping at the highest possible value for overflows, and the lowest possible value for underflows. + /// + Saturate, + /// + /// If an overflow is encountered, associated subcommand fails, and the result will be NULL. + /// + Fail, +} + +internal static class BitfieldOverflowHandlingExtensions +{ + internal static RedisValue AsRedisValue(this BitfieldOverflowHandling handling) => handling switch + { + BitfieldOverflowHandling.Fail => RedisLiterals.FAIL, + BitfieldOverflowHandling.Saturate => RedisLiterals.SAT, + BitfieldOverflowHandling.Wrap => RedisLiterals.WRAP, + _ => throw new ArgumentOutOfRangeException(nameof(handling)) + }; +} diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 728b88a76..ce671547e 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -13,6 +13,8 @@ internal enum RedisCommand BGREWRITEAOF, BGSAVE, BITCOUNT, + BITFIELD, + BITFIELD_RO, BITOP, BITPOS, BLPOP, @@ -263,6 +265,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) // for example spreading load via a .DemandReplica flag in the caller. // Basically: would it fail on a read-only replica in 100% of cases? Then it goes in the list. case RedisCommand.APPEND: + case RedisCommand.BITFIELD: case RedisCommand.BITOP: case RedisCommand.BLPOP: case RedisCommand.BRPOP: @@ -350,6 +353,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.BGREWRITEAOF: case RedisCommand.BGSAVE: case RedisCommand.BITCOUNT: + case RedisCommand.BITFIELD_RO: case RedisCommand.BITPOS: case RedisCommand.CLIENT: case RedisCommand.CLUSTER: diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index e5c120eb9..87ccf925e 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -2594,6 +2594,66 @@ IEnumerable SortedSetScan(RedisKey key, /// long StringBitCount(RedisKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None); + /// + /// Executes a set of Bitfield subcommands as constructed by the against the bitfield at the provided . + /// Will run as a BITFIELD_RO if all operations are read-only and the command is available. + /// + /// The key of the string. + /// The subcommands to execute against the bitfield. + /// The flags to use for this operation. + /// An array of numbers corresponding to the result of each sub-command. For increment subcommands, these can be null. + /// + /// , + /// + /// + long?[] StringBitfield(RedisKey key, BitfieldOperation[] subCommands, CommandFlags flags = CommandFlags.None); + + /// + /// Pulls a single number out of a bitfield of the provided encoding at the given offset. + /// Will execute a BITFIELD_RO if possible. + /// + /// The key for the string. + /// The offset into the bitfield to pull the number from. + /// The width of the encoding to interpret the bitfield width. + /// Whether or not to offset into the bitfield by bits vs encoding. + /// Whether or not to interpret the number gotten as an unsigned integer. + /// The flags to use for this operation. + /// The number of the given encoding at the provided . + /// + /// , + /// + /// + long StringBitfieldGet(RedisKey key, long offset, byte width, bool offsetByBit = true, bool unsigned = false, CommandFlags flags = CommandFlags.None); + + /// + /// Sets a single number in a bitfield at the provided offset to the provided, in the given encoding. + /// + /// The key for the string. + /// The offset into the bitfield to address. + /// The width of the encoding to interpret the bitfield width. + /// The value to set the addressed bits to. + /// Whether or not to offset into the bitfield by bits vs encoding. + /// Whether or not to interpret the number gotten as an unsigned integer. + /// The flags to use for this operation. + /// The previous value as an at the provided . + /// + long StringBitfieldSet(RedisKey key, long offset, byte width, long value, bool offsetByBit = true, bool unsigned = false, CommandFlags flags = CommandFlags.None); + + /// + /// Increments a single number in a bitfield at the provided in the provided encoding by the given . + /// + /// The key for the string. + /// The offset into the bitfield to address. + /// The width of the encoding to interpret the bitfield width. + /// The value to set the addressed bits to. + /// Whether or not to offset into the bitfield by bits vs encoding. + /// Whether or not to interpret the number gotten as an unsigned integer. + /// How to handle overflows. + /// The flags to use for this operation. + /// The new value of the given at the provided after the INCRBY is applied, represented as an . Returns if the operation fails. + /// + long? StringBitfieldIncrement(RedisKey key, long offset, byte width, long increment, bool offsetByBit = true, bool unsigned = false, BitfieldOverflowHandling overflowHandling = Redis.BitfieldOverflowHandling.Wrap, CommandFlags flags = CommandFlags.None); + /// /// Perform a bitwise operation between multiple keys (containing string values) and store the result in the destination key. /// The BITOP command supports four bitwise operations; note that NOT is a unary operator: the second key should be omitted in this case diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 4a9cd400d..269f40815 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -2547,6 +2547,66 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// Task StringBitCountAsync(RedisKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None); + /// + /// Executes a set of Bitfield subcommands as constructed by the against the bitfield at the provided . + /// Will run as a BITFIELD_RO if all operations are read-only and the command is available. + /// + /// The key of the string. + /// The subcommands to execute against the bitfield. + /// The flags to use for this operation. + /// An array of numbers corresponding to the result of each sub-command. For increment subcommands, these can be null. + /// + /// , + /// + /// + Task StringBitfieldAsync(RedisKey key, BitfieldOperation[] subCommands, CommandFlags flags = CommandFlags.None); + + /// + /// Pulls a single number out of a bitfield of the provided encoding at the given offset. + /// Will execute a BITFIELD_RO if possible. + /// + /// The key for the string. + /// The offset into the bitfield to address. + /// The width of the encoding to interpret the bitfield width. + /// Whether or not to offset into the bitfield by bits vs encoding. + /// Whether or not to interpret the number gotten as an unsigned integer. + /// The flags to use for this operation. + /// The number of the given encoding at the provided . + /// + /// , + /// + /// + Task StringBitfieldGetAsync(RedisKey key, long offset, byte width, bool offsetByBit = true, bool unsigned = false, CommandFlags flags = CommandFlags.None); + + /// + /// Sets a single number in a bitfield at the provided to the provided, in the given encoding. + /// + /// The key for the string. + /// The offset into the bitfield to address. + /// The width of the encoding to interpret the bitfield width. + /// The value to set the addressed bits to. + /// Whether or not to offset into the bitfield by bits vs encoding. + /// Whether or not to interpret the number gotten as an unsigned integer. + /// The flags to use for this operation. + /// The previous value as an at the provided . + /// + Task StringBitfieldSetAsync(RedisKey key, long offset, byte width, long value, bool offsetByBit = true, bool unsigned = false, CommandFlags flags = CommandFlags.None); + + /// + /// Increments a single number in a bitfield at the provided in the provided encoding by the given . + /// + /// The key for the string. + /// The offset into the bitfield to address. + /// The width of the encoding to interpret the bitfield width. + /// The value to set the addressed bits to. + /// Whether or not to offset into the bitfield by bits vs encoding. + /// Whether or not to interpret the number gotten as an unsigned integer. + /// How to handle overflows. + /// The flags to use for this operation. + /// The new value of the given at the provided after the INCRBY is applied, represented as an . Returns if the operation fails. + /// + Task StringBitfieldIncrementAsync(RedisKey key, long offset, byte width, long increment, bool offsetByBit = true, bool unsigned = false, BitfieldOverflowHandling overflowHandling = Redis.BitfieldOverflowHandling.Wrap, CommandFlags flags = CommandFlags.None); + /// /// Perform a bitwise operation between multiple keys (containing string values) and store the result in the destination key. /// The BITOP command supports four bitwise operations; note that NOT is a unary operator: the second key should be omitted in this case diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index e34cad895..70293d7ca 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -634,6 +634,18 @@ public Task StringBitCountAsync(RedisKey key, long start, long end, Comman public Task StringBitCountAsync(RedisKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => Inner.StringBitCountAsync(ToInner(key), start, end, indexType, flags); + public Task StringBitfieldGetAsync(RedisKey key, long offset, byte width, bool offsetByBit = true, bool unsigned = false, CommandFlags flags = CommandFlags.None) => + Inner.StringBitfieldGetAsync(ToInner(key), offset, width, offsetByBit, unsigned, flags); + + public Task StringBitfieldSetAsync(RedisKey key, long offset, byte width, long value, bool offsetByBit = true, bool unsigned = false, CommandFlags flags = CommandFlags.None) => + Inner.StringBitfieldSetAsync(ToInner(key), offset, width, value, offsetByBit, unsigned, flags); + + public Task StringBitfieldIncrementAsync(RedisKey key, long offset, byte width, long increment, bool offsetByBit = true, bool unsigned = false, BitfieldOverflowHandling overflowHandling = Redis.BitfieldOverflowHandling.Wrap, CommandFlags flags = CommandFlags.None) => + Inner.StringBitfieldIncrementAsync(ToInner(key), offset, width, increment, offsetByBit, unsigned, overflowHandling, flags); + + public Task StringBitfieldAsync(RedisKey key, BitfieldOperation[] subCommands, CommandFlags flags = CommandFlags.None) => + Inner.StringBitfieldAsync(ToInner(key), subCommands, flags); + public Task StringBitOperationAsync(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => Inner.StringBitOperationAsync(operation, ToInner(destination), ToInner(keys), flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index d1c47aeab..fe1eec523 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -618,6 +618,18 @@ public long StringBitCount(RedisKey key, long start, long end, CommandFlags flag public long StringBitCount(RedisKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => Inner.StringBitCount(ToInner(key), start, end, indexType, flags); + public long StringBitfieldGet(RedisKey key, long offset, byte width, bool offsetByBit = true, bool unsigned = false, CommandFlags flags = CommandFlags.None) => + Inner.StringBitfieldGet(ToInner(key), offset, width, offsetByBit, unsigned, flags); + + public long StringBitfieldSet(RedisKey key, long offset, byte width, long value, bool offsetByBit = true, bool unsigned = false, CommandFlags flags = CommandFlags.None) => + Inner.StringBitfieldSet(ToInner(key), offset, width, value, offsetByBit, unsigned, flags); + + public long? StringBitfieldIncrement(RedisKey key, long offset, byte width, long increment, bool offsetByBit = true, bool unsigned = false, BitfieldOverflowHandling overflowHandling = Redis.BitfieldOverflowHandling.Wrap, CommandFlags flags = CommandFlags.None) => + Inner.StringBitfieldIncrement(ToInner(key), offset, width, increment, offsetByBit, unsigned, overflowHandling, flags); + + public long?[] StringBitfield(RedisKey key, BitfieldOperation[] subCommands, CommandFlags flags = CommandFlags.None) => + Inner.StringBitfield(ToInner(key), subCommands, flags); + public long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => Inner.StringBitOperation(operation, ToInner(destination), ToInner(keys), flags); diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 459f66cf8..871d55de1 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -65,6 +65,10 @@ StackExchange.Redis.BacklogPolicy.AbortPendingOnConnectionFailure.init -> void StackExchange.Redis.BacklogPolicy.BacklogPolicy() -> void StackExchange.Redis.BacklogPolicy.QueueWhileDisconnected.get -> bool StackExchange.Redis.BacklogPolicy.QueueWhileDisconnected.init -> void +StackExchange.Redis.BitfieldOverflowHandling +StackExchange.Redis.BitfieldOverflowHandling.Fail = 2 -> StackExchange.Redis.BitfieldOverflowHandling +StackExchange.Redis.BitfieldOverflowHandling.Saturate = 1 -> StackExchange.Redis.BitfieldOverflowHandling +StackExchange.Redis.BitfieldOverflowHandling.Wrap = 0 -> StackExchange.Redis.BitfieldOverflowHandling StackExchange.Redis.Bitwise StackExchange.Redis.Bitwise.And = 0 -> StackExchange.Redis.Bitwise StackExchange.Redis.Bitwise.Not = 3 -> StackExchange.Redis.Bitwise diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index eff457070..36fe35ed2 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,9 +1,23 @@ abstract StackExchange.Redis.RedisResult.ToString(out string? type) -> string? override sealed StackExchange.Redis.RedisResult.ToString() -> string! override StackExchange.Redis.Role.Master.Replica.ToString() -> string! +StackExchange.Redis.BitfieldOperation +StackExchange.Redis.BitfieldOperation.BitfieldOperation() -> void +StackExchange.Redis.BitFieldSubCommand +StackExchange.Redis.BitFieldSubCommand.Get = 0 -> StackExchange.Redis.BitFieldSubCommand +StackExchange.Redis.BitFieldSubCommand.Increment = 2 -> StackExchange.Redis.BitFieldSubCommand +StackExchange.Redis.BitFieldSubCommand.Set = 1 -> StackExchange.Redis.BitFieldSubCommand StackExchange.Redis.ClientInfo.Protocol.get -> StackExchange.Redis.RedisProtocol? StackExchange.Redis.ConfigurationOptions.Protocol.get -> StackExchange.Redis.RedisProtocol? StackExchange.Redis.ConfigurationOptions.Protocol.set -> void +StackExchange.Redis.IDatabase.StringBitfield(StackExchange.Redis.RedisKey key, StackExchange.Redis.BitfieldOperation[]! subCommands, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long?[]! +StackExchange.Redis.IDatabase.StringBitfieldGet(StackExchange.Redis.RedisKey key, long offset, byte width, bool offsetByBit = true, bool unsigned = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StringBitfieldIncrement(StackExchange.Redis.RedisKey key, long offset, byte width, long increment, bool offsetByBit = true, bool unsigned = false, StackExchange.Redis.BitfieldOverflowHandling overflowHandling = StackExchange.Redis.BitfieldOverflowHandling.Wrap, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long? +StackExchange.Redis.IDatabase.StringBitfieldSet(StackExchange.Redis.RedisKey key, long offset, byte width, long value, bool offsetByBit = true, bool unsigned = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabaseAsync.StringBitfieldAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.BitfieldOperation[]! subCommands, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringBitfieldGetAsync(StackExchange.Redis.RedisKey key, long offset, byte width, bool offsetByBit = true, bool unsigned = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringBitfieldIncrementAsync(StackExchange.Redis.RedisKey key, long offset, byte width, long increment, bool offsetByBit = true, bool unsigned = false, StackExchange.Redis.BitfieldOverflowHandling overflowHandling = StackExchange.Redis.BitfieldOverflowHandling.Wrap, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringBitfieldSetAsync(StackExchange.Redis.RedisKey key, long offset, byte width, long value, bool offsetByBit = true, bool unsigned = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.Protocol.get -> StackExchange.Redis.RedisProtocol StackExchange.Redis.RedisFeatures.ClientId.get -> bool StackExchange.Redis.RedisFeatures.Equals(StackExchange.Redis.RedisFeatures other) -> bool @@ -25,6 +39,9 @@ StackExchange.Redis.ResultType.Null = 8 -> StackExchange.Redis.ResultType StackExchange.Redis.ResultType.Push = 37 -> StackExchange.Redis.ResultType StackExchange.Redis.ResultType.Set = 21 -> StackExchange.Redis.ResultType StackExchange.Redis.ResultType.VerbatimString = 12 -> StackExchange.Redis.ResultType +static StackExchange.Redis.BitfieldOperation.Get(long offset, byte width, bool offsetByBit = true, bool unsigned = false) -> StackExchange.Redis.BitfieldOperation +static StackExchange.Redis.BitfieldOperation.Increment(long offset, byte width, long increment, bool offsetByBit = true, bool unsigned = false, StackExchange.Redis.BitfieldOverflowHandling overflowHandling = StackExchange.Redis.BitfieldOverflowHandling.Wrap) -> StackExchange.Redis.BitfieldOperation +static StackExchange.Redis.BitfieldOperation.Set(long offset, byte width, long value, bool offsetByBit = true, bool unsigned = false) -> StackExchange.Redis.BitfieldOperation static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisResult![]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult! static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult! virtual StackExchange.Redis.RedisResult.Length.get -> int diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index 1581c29c9..f8ef10784 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -320,6 +320,9 @@ internal bool GetBoolean() [MethodImpl(MethodImplOptions.AggressiveInlining)] internal double?[]? GetItemsAsDoubles() => this.ToArray((in RawResult x) => x.TryGetDouble(out double val) ? val : null); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal long?[]? GetItemsAsInt64s() => this.ToArray((in RawResult x) => x.TryGetInt64(out long val) ? val : null); + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal RedisKey[]? GetItemsAsKeys() => this.ToArray((in RawResult x) => x.AsRedisKey()); diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 85cf25576..6f4fbd4fd 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Threading.Tasks; using Pipelines.Sockets.Unofficial.Arenas; @@ -2919,6 +2920,54 @@ public Task StringBitCountAsync(RedisKey key, long start = 0, long end = - return ExecuteAsync(msg, ResultProcessor.Int64); } + public long?[] StringBitfield(RedisKey key, BitfieldOperation[] subCommands, CommandFlags flags = CommandFlags.None) + { + var msg = subCommands.BuildMessage(Database, key, flags, this, out var server); + return ExecuteSync(msg, ResultProcessor.NullableInt64Array, defaultValue: Array.Empty(), server: server); + } + + public Task StringBitfieldAsync(RedisKey key, BitfieldOperation[] subCommands, CommandFlags flags = CommandFlags.None) + { + var msg = subCommands.BuildMessage(Database, key, flags, this, out var server); + return ExecuteAsync(msg, ResultProcessor.NullableInt64Array, defaultValue: Array.Empty(), server: server); + } + + public long StringBitfieldGet(RedisKey key, long offset, byte width, bool offsetByBit = true, bool unsigned = false, CommandFlags flags = CommandFlags.None) + { + var msg = BitfieldOperation.Get(offset, width, offsetByBit, unsigned).BuildMessage(Database, key, flags, this, out var server); + return ExecuteSync(msg, ResultProcessor.Int64, server); + } + + public Task StringBitfieldGetAsync(RedisKey key, long offset, byte width, bool offsetByBit = true, bool unsigned = false, CommandFlags flags = CommandFlags.None) + { + var msg = BitfieldOperation.Get(offset, width, offsetByBit, unsigned).BuildMessage(Database, key, flags, this, out var server); + return ExecuteAsync(msg, ResultProcessor.Int64, server); + } + + public long StringBitfieldSet(RedisKey key, long offset, byte width, long value, bool offsetByBit = true, bool unsigned = false, CommandFlags flags = CommandFlags.None) + { + var msg = BitfieldOperation.Set(offset, width, value, offsetByBit, unsigned).BuildMessage(Database, key, flags, this, out var server); + return ExecuteSync(msg, ResultProcessor.Int64, server); + } + + public Task StringBitfieldSetAsync(RedisKey key, long offset, byte width, long value, bool offsetByBit = true, bool unsigned = false, CommandFlags flags = CommandFlags.None) + { + var msg = BitfieldOperation.Set(offset, width, value, offsetByBit, unsigned).BuildMessage(Database, key, flags, this, out var server); + return ExecuteAsync(msg, ResultProcessor.Int64, server); + } + + public long? StringBitfieldIncrement(RedisKey key, long offset, byte width, long increment, bool offsetByBit = true, bool unsigned = false, BitfieldOverflowHandling overflowHandling = Redis.BitfieldOverflowHandling.Wrap, CommandFlags flags = CommandFlags.None) + { + var msg = BitfieldOperation.Increment(offset,width, increment, offsetByBit, unsigned, overflowHandling).BuildMessage(Database, key, flags, this, out var server); + return ExecuteSync(msg, ResultProcessor.NullableInt64, server); + } + + public Task StringBitfieldIncrementAsync(RedisKey key, long offset, byte width, long increment, bool offsetByBit = true, bool unsigned = false, BitfieldOverflowHandling overflowHandling = Redis.BitfieldOverflowHandling.Wrap, CommandFlags flags = CommandFlags.None) + { + var msg = BitfieldOperation.Increment(offset,width, increment, offsetByBit, unsigned, overflowHandling).BuildMessage(Database, key, flags, this, out var server); + return ExecuteAsync(msg, ResultProcessor.NullableInt64, server); + } + public long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) { var msg = GetStringBitOperationMessage(operation, destination, first, second, flags); diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index 7e3bb77dc..774ec6bf2 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -253,6 +253,11 @@ public RedisFeatures(Version version) /// public bool KeyTouch => Version.IsAtLeast(v3_2_1); + /// + /// Is BITFIELD_RO available? + /// + internal bool ReadOnlyBitfield => Version > v6_2_0; + /// /// Does the server prefer 'replica' terminology - 'REPLICAOF', etc? /// diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 7f786c4d7..e01551339 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -87,6 +87,7 @@ public static readonly RedisValue ID = "ID", IDX = "IDX", IDLETIME = "IDLETIME", + INCRBY = "INCRBY", KEEPTTL = "KEEPTTL", KILL = "KILL", LATEST = "LATEST", @@ -113,6 +114,7 @@ public static readonly RedisValue NX = "NX", OBJECT = "OBJECT", OR = "OR", + OVERFLOW = "OVERFLOW", PATTERN = "PATTERN", PAUSE = "PAUSE", PERSIST = "PERSIST", @@ -187,6 +189,11 @@ public static readonly RedisValue m = "m", mi = "mi", + //Bitfield literals + FAIL = "FAIL", + SAT = "SAT", + WRAP = "WRAP", + // misc (config, etc) databases = "databases", master = "master", diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 6fd229af1..ff23c245b 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -65,6 +65,9 @@ public static readonly ResultProcessor public static readonly ResultProcessor NullableDoubleArray = new NullableDoubleArrayProcessor(); + public static readonly ResultProcessor + NullableInt64Array = new NullableInt64ArrayProcessor(); + public static readonly ResultProcessor NullableInt64 = new NullableInt64Processor(); @@ -1347,6 +1350,14 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes SetResult(message, i64); return true; } + break; + case ResultType.Array: + if (result.GetItems()[0].TryGetInt64(out i64)) + { + SetResult(message, i64); + return true; + } + break; } return false; @@ -1370,6 +1381,21 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + + private sealed class NullableInt64ArrayProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray == ResultType.Array && !result.IsNull) + { + var arr = result.GetItemsAsInt64s()!; + SetResult(message, arr); + return true; + } + return false; + } + } + private sealed class NullableDoubleArrayProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) @@ -1430,6 +1456,21 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes SetResult(message, i64); return true; } + break; + case ResultType.Array: + var item = result.GetItems()[0]; + if (item.IsNull) + { + SetResult(message, null); + return true; + } + + if (item.TryGetInt64(out i64)) + { + SetResult(message, i64); + return true; + } + break; } return false; diff --git a/tests/StackExchange.Redis.Tests/Bitfield.cs b/tests/StackExchange.Redis.Tests/Bitfield.cs new file mode 100644 index 000000000..517c53bb7 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Bitfield.cs @@ -0,0 +1,148 @@ +using System; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Bitfield : TestBase +{ + public Bitfield(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public void TestBitfieldHappyPath() + { + using var conn = Create(require: RedisFeatures.v3_2_0); + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key); + + var offset = 1; + byte width = 10; + var byBit = false; + var unsigned = false; + + + // should be the old value + var setResult = db.StringBitfieldSet(key, offset, width, -255, byBit, unsigned); + var getResult = db.StringBitfieldGet(key, offset, width, byBit, unsigned); + var incrementResult = db.StringBitfieldIncrement(key, offset, width, -10, byBit, unsigned); + Assert.Equal(0, setResult); + Assert.Equal(-255, getResult); + Assert.Equal(-265, incrementResult); + + width = 18; + unsigned = true; + offset = 22; + byBit = true; + + setResult = db.StringBitfieldSet(key, offset, width, 262123, byBit, unsigned); + getResult = db.StringBitfieldGet(key, offset, width, byBit, unsigned); + incrementResult = db.StringBitfieldIncrement(key, offset, width, 20, byBit, unsigned); + + Assert.Equal(0, setResult); + Assert.Equal(262123, getResult); + Assert.Equal(262143, incrementResult); + } + + [Fact] + public async Task TestBitfieldHappyPathAsync() + { + using var conn = Create(require: RedisFeatures.v3_2_0); + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key); + + var offset = 1; + byte width = 10; + var byBit = false; + var unsigned = false; + + // should be the old value + var setResult = await db.StringBitfieldSetAsync(key, offset, width, -255, byBit, unsigned); + var getResult = await db.StringBitfieldGetAsync(key, offset, width, byBit, unsigned); + var incrementResult = await db.StringBitfieldIncrementAsync(key, offset, width, -10, byBit, unsigned); + Assert.Equal(0, setResult); + Assert.Equal(-255, getResult); + Assert.Equal(-265, incrementResult); + + width = 18; + unsigned = true; + offset = 22; + byBit = true; + + setResult = await db.StringBitfieldSetAsync(key, offset, width, 262123, byBit, unsigned); + getResult = await db.StringBitfieldGetAsync(key, offset, width, byBit, unsigned); + incrementResult = await db.StringBitfieldIncrementAsync(key, offset, width, 20, byBit, unsigned); + + Assert.Equal(0, setResult); + Assert.Equal(262123, getResult); + Assert.Equal(262143, incrementResult); + } + + [Fact] + public async Task TestBitfieldMulti() + { + using var conn = Create(require: RedisFeatures.v3_2_0); + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key); + + var subCommands = new[] + { + BitfieldOperation.Set(5, 3, 7, true, true), + BitfieldOperation.Get(5, 3, true, true), + BitfieldOperation.Increment(5, 3, -1, true, true), + BitfieldOperation.Set(1, 45, 17592186044415, false, false), + BitfieldOperation.Get(1, 45, false, false), + BitfieldOperation.Increment(1, 45, 1, false, false, BitfieldOverflowHandling.Fail) + }; + + var res = await db.StringBitfieldAsync(key, subCommands); + + Assert.Equal(0, res[0]); + Assert.Equal(7, res[1]); + Assert.Equal(6, res[2]); + Assert.Equal(0, res[3]); + Assert.Equal(17592186044415, res[4]); + Assert.Null(res[5]); + } + + [Fact] + public async Task TestOverflows() + { + using var conn = Create(require: RedisFeatures.v3_2_0); + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key); + + var offset = 3; + byte width = 3; + var byBit = false; + var unsigned = false; + + await db.StringBitfieldSetAsync(key, offset, width, 3, byBit, unsigned); + var incrFail = await db.StringBitfieldIncrementAsync(key, offset, width, 1, byBit, unsigned, BitfieldOverflowHandling.Fail); + Assert.Null(incrFail); + var incrWrap = await db.StringBitfieldIncrementAsync(key, offset, width, 1, byBit, unsigned); + Assert.Equal(-4, incrWrap); + await db.StringBitfieldSetAsync(key, offset, width, 3, byBit, unsigned); + var incrSat = await db.StringBitfieldIncrementAsync(key, offset, width, 1, byBit, unsigned, BitfieldOverflowHandling.Saturate); + Assert.Equal(3, incrSat); + } + + [Fact] + public void PreflightValidation() + { + using var conn = Create(require: RedisFeatures.v3_2_0); + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key); + + Assert.Throws(()=> db.StringBitfieldGet(key, 0, 0, false, false)); + Assert.Throws(()=> db.StringBitfieldGet(key, 64, 0, false, true)); + Assert.Throws(()=> db.StringBitfieldGet(key, 65, 0, false, false)); + Assert.Throws(() => db.StringBitfield(key, new[] { new BitfieldOperation() { Offset = "0", SubCommand = BitFieldSubCommand.Set, Encoding = "i5" } })); + } +} diff --git a/tests/StackExchange.Redis.Tests/StringTests.cs b/tests/StackExchange.Redis.Tests/StringTests.cs index 23acf737f..b37f35f2b 100644 --- a/tests/StackExchange.Redis.Tests/StringTests.cs +++ b/tests/StackExchange.Redis.Tests/StringTests.cs @@ -670,7 +670,6 @@ public async Task HashStringLengthAsync() Assert.Equal(0, await resNonExistingAsync); } - [Fact] public void HashStringLength() {