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()
{