Repository: redis/lettuce
Pull request #3334 (2025-06-25 11:59:09)

Add support for new operations of BITOP command in Redis Community Edition 8.2
[Redis CE 8.2 ](https://github.com/redis/redis/releases/tag/8.2-m01) supports new operations often required with BITOP command;


**DIFF(X, Y1, Y2, …)** 
_members of X that are not members of any of Y1, Y2 ,…_
_X  ∧ ¬(Y1 ∨ Y2 ∨ …)_


**DIFF1(X, Y1, Y2, …)** 
_members of one or more of Y1, Y2 ,… that are not members of X_
_¬X  ∧ (Y1 ∨ Y2 ∨ …)_


**ANDOR(X, Y1, Y2, …)** 
_members of X that are also members of one or more of Y1, Y2 ,…_
_X  ∧ (Y1 ∨ Y2 ∨ …)_


**ONE(X1, X2, …) -** 
_members of exactly one of X1, X2, …_
_For two bitmaps this is equivalent to XOR(X1, X2) (AKA symmetric difference., X1 ⊕ X2). However, for more than two bitmaps, XOR has little practical use._
_In general this is COUNT(X1, X2, …) == 1_

See more details [here](https://github.com/redis/redis/pull/13898).


uglide: LGTM. @tishun WDYT?

Patch:
diff --git a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
index 0f5d755f4..1566578e2 100644
--- a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
+++ b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
@@ -286,6 +286,26 @@ public abstract class AbstractRedisAsyncCommands<K, V> implements RedisAclAsyncC
         return dispatch(commandBuilder.bitopXor(destination, keys));
     }
 
+    @Override
+    public RedisFuture<Long> bitopDiff(K destination, K sourceKey, K... keys) {
+        return dispatch(commandBuilder.bitopDiff(destination, sourceKey, keys));
+    }
+
+    @Override
+    public RedisFuture<Long> bitopDiff1(K destination, K sourceKey, K... keys) {
+        return dispatch(commandBuilder.bitopDiff1(destination, sourceKey, keys));
+    }
+
+    @Override
+    public RedisFuture<Long> bitopAndor(K destination, K sourceKey, K... keys) {
+        return dispatch(commandBuilder.bitopAndor(destination, sourceKey, keys));
+    }
+
+    @Override
+    public RedisFuture<Long> bitopOne(K destination, K... keys) {
+        return dispatch(commandBuilder.bitopOne(destination, keys));
+    }
+
     @Override
     public RedisFuture<Long> bitpos(K key, boolean state) {
         return dispatch(commandBuilder.bitpos(key, state));
diff --git a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
index 779d7c9b1..6e7fe9bb6 100644
--- a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
+++ b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
@@ -308,6 +308,26 @@ public abstract class AbstractRedisReactiveCommands<K, V> implements RedisAclRea
         return createMono(() -> commandBuilder.bitopXor(destination, keys));
     }
 
+    @Override
+    public Mono<Long> bitopDiff(K destination, K sourceKey, K... keys) {
+        return createMono(() -> commandBuilder.bitopDiff(destination, sourceKey, keys));
+    }
+
+    @Override
+    public Mono<Long> bitopDiff1(K destination, K sourceKey, K... keys) {
+        return createMono(() -> commandBuilder.bitopDiff1(destination, sourceKey, keys));
+    }
+
+    @Override
+    public Mono<Long> bitopAndor(K destination, K sourceKey, K... keys) {
+        return createMono(() -> commandBuilder.bitopAndor(destination, sourceKey, keys));
+    }
+
+    @Override
+    public Mono<Long> bitopOne(K destination, K... keys) {
+        return createMono(() -> commandBuilder.bitopOne(destination, keys));
+    }
+
     @Override
     public Mono<Long> bitpos(K key, boolean state) {
         return createMono(() -> commandBuilder.bitpos(key, state));
diff --git a/src/main/java/io/lettuce/core/RedisCommandBuilder.java b/src/main/java/io/lettuce/core/RedisCommandBuilder.java
index 6bcf4fa1b..9c8da12c0 100644
--- a/src/main/java/io/lettuce/core/RedisCommandBuilder.java
+++ b/src/main/java/io/lettuce/core/RedisCommandBuilder.java
@@ -321,6 +321,45 @@ class RedisCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
         return createCommand(BITOP, new IntegerOutput<>(codec), args);
     }
 
+    Command<K, V, Long> bitopDiff(K destination, K sourceKey, K... keys) {
+        LettuceAssert.notNull(destination, "Destination " + MUST_NOT_BE_NULL);
+        LettuceAssert.notNull(sourceKey, "Source key " + MUST_NOT_BE_NULL);
+        notEmpty(keys);
+
+        CommandArgs<K, V> args = new CommandArgs<>(codec);
+        args.add(DIFF).addKey(destination).addKey(sourceKey).addKeys(keys);
+        return createCommand(BITOP, new IntegerOutput<>(codec), args);
+    }
+
+    Command<K, V, Long> bitopDiff1(K destination, K sourceKey, K... keys) {
+        LettuceAssert.notNull(destination, "Destination " + MUST_NOT_BE_NULL);
+        LettuceAssert.notNull(sourceKey, "Source key " + MUST_NOT_BE_NULL);
+        notEmpty(keys);
+
+        CommandArgs<K, V> args = new CommandArgs<>(codec);
+        args.add(DIFF1).addKey(destination).addKey(sourceKey).addKeys(keys);
+        return createCommand(BITOP, new IntegerOutput<>(codec), args);
+    }
+
+    Command<K, V, Long> bitopAndor(K destination, K sourceKey, K... keys) {
+        LettuceAssert.notNull(destination, "Destination " + MUST_NOT_BE_NULL);
+        LettuceAssert.notNull(sourceKey, "Source key " + MUST_NOT_BE_NULL);
+        notEmpty(keys);
+
+        CommandArgs<K, V> args = new CommandArgs<>(codec);
+        args.add(ANDOR).addKey(destination).addKey(sourceKey).addKeys(keys);
+        return createCommand(BITOP, new IntegerOutput<>(codec), args);
+    }
+
+    Command<K, V, Long> bitopOne(K destination, K... keys) {
+        LettuceAssert.notNull(destination, "Destination " + MUST_NOT_BE_NULL);
+        notEmpty(keys);
+
+        CommandArgs<K, V> args = new CommandArgs<>(codec);
+        args.add(ONE).addKey(destination).addKeys(keys);
+        return createCommand(BITOP, new IntegerOutput<>(codec), args);
+    }
+
     Command<K, V, Long> bitpos(K key, boolean state) {
         notNullKey(key);
 
diff --git a/src/main/java/io/lettuce/core/api/async/RedisStringAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RedisStringAsyncCommands.java
index 7b524c092..bcacfa731 100644
--- a/src/main/java/io/lettuce/core/api/async/RedisStringAsyncCommands.java
+++ b/src/main/java/io/lettuce/core/api/async/RedisStringAsyncCommands.java
@@ -184,6 +184,53 @@ public interface RedisStringAsyncCommands<K, V> {
      */
     RedisFuture<Long> bitopXor(K destination, K... keys);
 
+    /**
+     * Perform bitwise DIFF between strings. Members of the source key that are not members of any of the other keys. Equivalent
+     * to: X ∧ ¬(Y1 ∨ Y2 ∨ …)
+     *
+     * @param destination result key of the operation.
+     * @param sourceKey the source key (X) for comparison.
+     * @param keys one or more additional keys (Y1, Y2, ...). At least one key is required.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    RedisFuture<Long> bitopDiff(K destination, K sourceKey, K... keys);
+
+    /**
+     * Perform bitwise DIFF1 between strings. Members of one or more of the keys that are not members of the source key.
+     * Equivalent to: ¬X ∧ (Y1 ∨ Y2 ∨ …)
+     *
+     * @param destination result key of the operation.
+     * @param sourceKey the source key (X) for comparison.
+     * @param keys one or more additional keys (Y1, Y2, ...). At least one key is required.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    RedisFuture<Long> bitopDiff1(K destination, K sourceKey, K... keys);
+
+    /**
+     * Perform bitwise ANDOR between strings. Members of the source key that are also members of one or more of the other keys.
+     * Equivalent to: X ∧ (Y1 ∨ Y2 ∨ …)
+     *
+     * @param destination result key of the operation.
+     * @param sourceKey the source key (X) for comparison.
+     * @param keys one or more additional keys (Y1, Y2, ...). At least one key is required.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    RedisFuture<Long> bitopAndor(K destination, K sourceKey, K... keys);
+
+    /**
+     * Perform bitwise ONE between strings. Members of exactly one of the given keys. For two keys this is equivalent to XOR.
+     * For more than two keys, returns members that appear in exactly one key.
+     *
+     * @param destination result key of the operation.
+     * @param keys operation input key names.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    RedisFuture<Long> bitopOne(K destination, K... keys);
+
     /**
      * Decrement the integer value of a key by one.
      *
diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisStringReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisStringReactiveCommands.java
index e7722b439..60e2d5294 100644
--- a/src/main/java/io/lettuce/core/api/reactive/RedisStringReactiveCommands.java
+++ b/src/main/java/io/lettuce/core/api/reactive/RedisStringReactiveCommands.java
@@ -185,6 +185,53 @@ public interface RedisStringReactiveCommands<K, V> {
      */
     Mono<Long> bitopXor(K destination, K... keys);
 
+    /**
+     * Perform bitwise DIFF between strings. Members of the source key that are not members of any of the other keys. Equivalent
+     * to: X ∧ ¬(Y1 ∨ Y2 ∨ …)
+     *
+     * @param destination result key of the operation.
+     * @param sourceKey the source key (X) for comparison.
+     * @param keys one or more additional keys (Y1, Y2, ...). At least one key is required.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    Mono<Long> bitopDiff(K destination, K sourceKey, K... keys);
+
+    /**
+     * Perform bitwise DIFF1 between strings. Members of one or more of the keys that are not members of the source key.
+     * Equivalent to: ¬X ∧ (Y1 ∨ Y2 ∨ …)
+     *
+     * @param destination result key of the operation.
+     * @param sourceKey the source key (X) for comparison.
+     * @param keys one or more additional keys (Y1, Y2, ...). At least one key is required.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    Mono<Long> bitopDiff1(K destination, K sourceKey, K... keys);
+
+    /**
+     * Perform bitwise ANDOR between strings. Members of the source key that are also members of one or more of the other keys.
+     * Equivalent to: X ∧ (Y1 ∨ Y2 ∨ …)
+     *
+     * @param destination result key of the operation.
+     * @param sourceKey the source key (X) for comparison.
+     * @param keys one or more additional keys (Y1, Y2, ...). At least one key is required.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    Mono<Long> bitopAndor(K destination, K sourceKey, K... keys);
+
+    /**
+     * Perform bitwise ONE between strings. Members of exactly one of the given keys. For two keys this is equivalent to XOR.
+     * For more than two keys, returns members that appear in exactly one key.
+     *
+     * @param destination result key of the operation.
+     * @param keys operation input key names.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    Mono<Long> bitopOne(K destination, K... keys);
+
     /**
      * Decrement the integer value of a key by one.
      *
diff --git a/src/main/java/io/lettuce/core/api/sync/RedisStringCommands.java b/src/main/java/io/lettuce/core/api/sync/RedisStringCommands.java
index df13a42ae..bae38567b 100644
--- a/src/main/java/io/lettuce/core/api/sync/RedisStringCommands.java
+++ b/src/main/java/io/lettuce/core/api/sync/RedisStringCommands.java
@@ -183,6 +183,53 @@ public interface RedisStringCommands<K, V> {
      */
     Long bitopXor(K destination, K... keys);
 
+    /**
+     * Perform bitwise DIFF between strings. Members of the source key that are not members of any of the other keys. Equivalent
+     * to: X ∧ ¬(Y1 ∨ Y2 ∨ …)
+     *
+     * @param destination result key of the operation.
+     * @param sourceKey the source key (X) for comparison.
+     * @param keys one or more additional keys (Y1, Y2, ...). At least one key is required.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    Long bitopDiff(K destination, K sourceKey, K... keys);
+
+    /**
+     * Perform bitwise DIFF1 between strings. Members of one or more of the keys that are not members of the source key.
+     * Equivalent to: ¬X ∧ (Y1 ∨ Y2 ∨ …)
+     *
+     * @param destination result key of the operation.
+     * @param sourceKey the source key (X) for comparison.
+     * @param keys one or more additional keys (Y1, Y2, ...). At least one key is required.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    Long bitopDiff1(K destination, K sourceKey, K... keys);
+
+    /**
+     * Perform bitwise ANDOR between strings. Members of the source key that are also members of one or more of the other keys.
+     * Equivalent to: X ∧ (Y1 ∨ Y2 ∨ …)
+     *
+     * @param destination result key of the operation.
+     * @param sourceKey the source key (X) for comparison.
+     * @param keys one or more additional keys (Y1, Y2, ...). At least one key is required.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    Long bitopAndor(K destination, K sourceKey, K... keys);
+
+    /**
+     * Perform bitwise ONE between strings. Members of exactly one of the given keys. For two keys this is equivalent to XOR.
+     * For more than two keys, returns members that appear in exactly one key.
+     *
+     * @param destination result key of the operation.
+     * @param keys operation input key names.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    Long bitopOne(K destination, K... keys);
+
     /**
      * Decrement the integer value of a key by one.
      *
diff --git a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionStringAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionStringAsyncCommands.java
index 93e1701b1..44672f381 100644
--- a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionStringAsyncCommands.java
+++ b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionStringAsyncCommands.java
@@ -183,6 +183,53 @@ public interface NodeSelectionStringAsyncCommands<K, V> {
      */
     AsyncExecutions<Long> bitopXor(K destination, K... keys);
 
+    /**
+     * Perform bitwise DIFF between strings. Members of the source key that are not members of any of the other keys. Equivalent
+     * to: X ∧ ¬(Y1 ∨ Y2 ∨ …)
+     *
+     * @param destination result key of the operation.
+     * @param sourceKey the source key (X) for comparison.
+     * @param keys one or more additional keys (Y1, Y2, ...). At least one key is required.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    AsyncExecutions<Long> bitopDiff(K destination, K sourceKey, K... keys);
+
+    /**
+     * Perform bitwise DIFF1 between strings. Members of one or more of the keys that are not members of the source key.
+     * Equivalent to: ¬X ∧ (Y1 ∨ Y2 ∨ …)
+     *
+     * @param destination result key of the operation.
+     * @param sourceKey the source key (X) for comparison.
+     * @param keys one or more additional keys (Y1, Y2, ...). At least one key is required.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    AsyncExecutions<Long> bitopDiff1(K destination, K sourceKey, K... keys);
+
+    /**
+     * Perform bitwise ANDOR between strings. Members of the source key that are also members of one or more of the other keys.
+     * Equivalent to: X ∧ (Y1 ∨ Y2 ∨ …)
+     *
+     * @param destination result key of the operation.
+     * @param sourceKey the source key (X) for comparison.
+     * @param keys one or more additional keys (Y1, Y2, ...). At least one key is required.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    AsyncExecutions<Long> bitopAndor(K destination, K sourceKey, K... keys);
+
+    /**
+     * Perform bitwise ONE between strings. Members of exactly one of the given keys. For two keys this is equivalent to XOR.
+     * For more than two keys, returns members that appear in exactly one key.
+     *
+     * @param destination result key of the operation.
+     * @param keys operation input key names.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    AsyncExecutions<Long> bitopOne(K destination, K... keys);
+
     /**
      * Decrement the integer value of a key by one.
      *
diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionStringCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionStringCommands.java
index dcf3f27ac..d52a332d5 100644
--- a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionStringCommands.java
+++ b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionStringCommands.java
@@ -183,6 +183,53 @@ public interface NodeSelectionStringCommands<K, V> {
      */
     Executions<Long> bitopXor(K destination, K... keys);
 
+    /**
+     * Perform bitwise DIFF between strings. Members of the source key that are not members of any of the other keys. Equivalent
+     * to: X ∧ ¬(Y1 ∨ Y2 ∨ …)
+     *
+     * @param destination result key of the operation.
+     * @param sourceKey the source key (X) for comparison.
+     * @param keys one or more additional keys (Y1, Y2, ...). At least one key is required.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    Executions<Long> bitopDiff(K destination, K sourceKey, K... keys);
+
+    /**
+     * Perform bitwise DIFF1 between strings. Members of one or more of the keys that are not members of the source key.
+     * Equivalent to: ¬X ∧ (Y1 ∨ Y2 ∨ …)
+     *
+     * @param destination result key of the operation.
+     * @param sourceKey the source key (X) for comparison.
+     * @param keys one or more additional keys (Y1, Y2, ...). At least one key is required.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    Executions<Long> bitopDiff1(K destination, K sourceKey, K... keys);
+
+    /**
+     * Perform bitwise ANDOR between strings. Members of the source key that are also members of one or more of the other keys.
+     * Equivalent to: X ∧ (Y1 ∨ Y2 ∨ …)
+     *
+     * @param destination result key of the operation.
+     * @param sourceKey the source key (X) for comparison.
+     * @param keys one or more additional keys (Y1, Y2, ...). At least one key is required.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    Executions<Long> bitopAndor(K destination, K sourceKey, K... keys);
+
+    /**
+     * Perform bitwise ONE between strings. Members of exactly one of the given keys. For two keys this is equivalent to XOR.
+     * For more than two keys, returns members that appear in exactly one key.
+     *
+     * @param destination result key of the operation.
+     * @param keys operation input key names.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    Executions<Long> bitopOne(K destination, K... keys);
+
     /**
      * Decrement the integer value of a key by one.
      *
diff --git a/src/main/java/io/lettuce/core/protocol/CommandKeyword.java b/src/main/java/io/lettuce/core/protocol/CommandKeyword.java
index 5ccfc6035..628365ca4 100644
--- a/src/main/java/io/lettuce/core/protocol/CommandKeyword.java
+++ b/src/main/java/io/lettuce/core/protocol/CommandKeyword.java
@@ -33,9 +33,9 @@ import java.nio.charset.StandardCharsets;
  */
 public enum CommandKeyword implements ProtocolKeyword {
 
-    ABSTTL, ADDR, ADDSLOTS, ADDSLOTSRANGE, AFTER, AGGREGATE, ALLCHANNELS, ALLCOMMANDS, ALLKEYS, ALPHA, AND, ASK, ASC, ASYNC, BEFORE, BLOCK, BUMPEPOCH,
+    ABSTTL, ADDR, ADDSLOTS, ADDSLOTSRANGE, AFTER, AGGREGATE, ALLCHANNELS, ALLCOMMANDS, ALLKEYS, ALPHA, AND, ANDOR, ASK, ASC, ASYNC, BEFORE, BLOCK, BUMPEPOCH,
 
-    BY, BYLEX, BYSCORE, CACHING, CAT, CH, CHANNELS, COPY, COUNT, COUNTKEYSINSLOT, CONSUMERS, CREATE, DB, DELSLOTS, DELSLOTSRANGE, DELUSER, DESC, DRYRUN, SOFT, HARD, ENCODING,
+    BY, BYLEX, BYSCORE, CACHING, CAT, CH, CHANNELS, COPY, COUNT, COUNTKEYSINSLOT, CONSUMERS, CREATE, DB, DELSLOTS, DELSLOTSRANGE, DELUSER, DESC, DIFF, DIFF1, DRYRUN, SOFT, HARD, ENCODING,
 
     FAILOVER, FORGET, FIELDS, FLAGS, FLUSH, FORCE, FREQ, FLUSHSLOTS, GENPASS, GETNAME, GETUSER, GETKEYSINSLOT, GETREDIR, GROUP, GROUPS, HTSTATS, ID, IDLE, IDX, INFO,
 
diff --git a/src/main/templates/io/lettuce/core/api/RedisStringCommands.java b/src/main/templates/io/lettuce/core/api/RedisStringCommands.java
index 02a8a2562..d4530aa88 100644
--- a/src/main/templates/io/lettuce/core/api/RedisStringCommands.java
+++ b/src/main/templates/io/lettuce/core/api/RedisStringCommands.java
@@ -176,6 +176,53 @@ public interface RedisStringCommands<K, V> {
      */
     Long bitopXor(K destination, K... keys);
 
+    /**
+     * Perform bitwise DIFF between strings. Members of the source key that are not members of any of the other keys. Equivalent
+     * to: X ∧ ¬(Y1 ∨ Y2 ∨ …)
+     *
+     * @param destination result key of the operation.
+     * @param sourceKey the source key (X) for comparison.
+     * @param keys one or more additional keys (Y1, Y2, ...). At least one key is required.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    Long bitopDiff(K destination, K sourceKey, K... keys);
+
+    /**
+     * Perform bitwise DIFF1 between strings. Members of one or more of the keys that are not members of the source key.
+     * Equivalent to: ¬X ∧ (Y1 ∨ Y2 ∨ …)
+     *
+     * @param destination result key of the operation.
+     * @param sourceKey the source key (X) for comparison.
+     * @param keys one or more additional keys (Y1, Y2, ...). At least one key is required.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    Long bitopDiff1(K destination, K sourceKey, K... keys);
+
+    /**
+     * Perform bitwise ANDOR between strings. Members of the source key that are also members of one or more of the other keys.
+     * Equivalent to: X ∧ (Y1 ∨ Y2 ∨ …)
+     *
+     * @param destination result key of the operation.
+     * @param sourceKey the source key (X) for comparison.
+     * @param keys one or more additional keys (Y1, Y2, ...). At least one key is required.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    Long bitopAndor(K destination, K sourceKey, K... keys);
+
+    /**
+     * Perform bitwise ONE between strings. Members of exactly one of the given keys. For two keys this is equivalent to XOR.
+     * For more than two keys, returns members that appear in exactly one key.
+     *
+     * @param destination result key of the operation.
+     * @param keys operation input key names.
+     * @return Long integer-reply The size of the string stored in the destination key, that is equal to the size of the longest
+     *         input string.
+     */
+    Long bitopOne(K destination, K... keys);
+
     /**
      * Decrement the integer value of a key by one.
      *


Test patch:
diff --git a/src/test/java/io/lettuce/core/RedisCommandBuilderUnitTests.java b/src/test/java/io/lettuce/core/RedisCommandBuilderUnitTests.java
index a02ae2194..b6df2bd10 100644
--- a/src/test/java/io/lettuce/core/RedisCommandBuilderUnitTests.java
+++ b/src/test/java/io/lettuce/core/RedisCommandBuilderUnitTests.java
@@ -253,4 +253,48 @@ class RedisCommandBuilderUnitTests {
         assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*2\r\n$7\r\nCLUSTER\r\n$5\r\nLINKS\r\n");
     }
 
+    @Test
+    void shouldCorrectlyConstructBitopDiff() {
+
+        Command<String, String, ?> command = sut.bitopDiff("dest", "key1", "key2", "key3");
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8))
+                .isEqualTo("*6\r\n$5\r\nBITOP\r\n$4\r\nDIFF\r\n$4\r\ndest\r\n$4\r\nkey1\r\n$4\r\nkey2\r\n$4\r\nkey3\r\n");
+    }
+
+    @Test
+    void shouldCorrectlyConstructBitopDiff1() {
+
+        Command<String, String, ?> command = sut.bitopDiff1("dest", "key1", "key2", "key3");
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8))
+                .isEqualTo("*6\r\n$5\r\nBITOP\r\n$5\r\nDIFF1\r\n$4\r\ndest\r\n$4\r\nkey1\r\n$4\r\nkey2\r\n$4\r\nkey3\r\n");
+    }
+
+    @Test
+    void shouldCorrectlyConstructBitopAndor() {
+
+        Command<String, String, ?> command = sut.bitopAndor("dest", "key1", "key2", "key3");
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8))
+                .isEqualTo("*6\r\n$5\r\nBITOP\r\n$5\r\nANDOR\r\n$4\r\ndest\r\n$4\r\nkey1\r\n$4\r\nkey2\r\n$4\r\nkey3\r\n");
+    }
+
+    @Test
+    void shouldCorrectlyConstructBitopOne() {
+
+        Command<String, String, ?> command = sut.bitopOne("dest", "key1", "key2", "key3");
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        assertThat(buf.toString(StandardCharsets.UTF_8))
+                .isEqualTo("*6\r\n$5\r\nBITOP\r\n$3\r\nONE\r\n$4\r\ndest\r\n$4\r\nkey1\r\n$4\r\nkey2\r\n$4\r\nkey3\r\n");
+    }
+
 }
diff --git a/src/test/java/io/lettuce/core/commands/BitCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/BitCommandIntegrationTests.java
index 16ae65c3f..3506feb98 100644
--- a/src/test/java/io/lettuce/core/commands/BitCommandIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/commands/BitCommandIntegrationTests.java
@@ -27,6 +27,7 @@ import static io.lettuce.core.BitFieldArgs.unsigned;
 import static io.lettuce.core.BitFieldArgs.OverflowType.WRAP;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
 
 import java.util.List;
 
@@ -43,8 +44,11 @@ import io.lettuce.core.BitFieldArgs;
 import io.lettuce.core.RedisClient;
 import io.lettuce.core.TestSupport;
 import io.lettuce.core.api.sync.RedisCommands;
+import io.lettuce.core.codec.RedisCodec;
 import io.lettuce.test.LettuceExtension;
 import io.lettuce.test.condition.EnabledOnCommand;
+import io.lettuce.test.condition.RedisConditions;
+import io.netty.buffer.ByteBuf;
 
 /**
  * @author Will Glozer
@@ -59,6 +63,23 @@ public class BitCommandIntegrationTests extends TestSupport {
 
     private final RedisCommands<String, String> redis;
 
+    private RedisCodec<String, String> oneByteStringCodec = new BitStringCodec() {
+
+        @Override
+        public void encodeValue(String oneByte, ByteBuf target) {
+            byte value = 0;
+            for (int i = 0; i < 8; i++) {
+                if (oneByte.charAt(i) == '1') {
+                    value |= (1 << i);
+                }
+            }
+            target.writeByte(value);
+        }
+
+    };
+
+    private RedisCommands<String, String> byteString;
+
     protected RedisCommands<String, String> bitstring;
 
     @Inject
@@ -71,6 +92,7 @@ public class BitCommandIntegrationTests extends TestSupport {
     void setUp() {
         this.redis.flushall();
         this.bitstring = client.connect(new BitStringCodec()).sync();
+        byteString = client.connect(oneByteStringCodec).sync();
     }
 
     @AfterEach
@@ -308,6 +330,221 @@ public class BitCommandIntegrationTests extends TestSupport {
         assertThat(bitstring.get(key)).isEqualTo("00000100");
     }
 
+    @Test
+    void bitopDiff() {
+        assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("8.1.240"));
+        // Test DIFF: X ∧ ¬(Y1 ∨ Y2 ∨ …)
+        // Set up test data: foo has bits 0,1,2 set, bar has bit 1 set, baz has bit 2 set
+        byteString.set("foo", "00000111");
+        byteString.set("bar", "00000010");
+        byteString.set("baz", "00000100");
+
+        // DIFF should return bits in foo that are not in bar OR baz
+        // foo: 00000111 (bits 0,1,2 set), bar: 00000010 (bit 1 set), baz: 00000100 (bit 2 set)
+        // bar OR baz: 00000110
+        // foo AND NOT(bar OR baz): 00000001
+        assertThat(redis.bitopDiff(key, "foo", "bar", "baz")).isEqualTo(1);
+        assertThat(bitstring.get(key)).isEqualTo("00000001");
+    }
+
+    @Test
+    void bitopDiff1() {
+        assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("8.1.240"));
+        // Test DIFF1: ¬X ∧ (Y1 ∨ Y2 ∨ …)
+        // Set up test data: foo has bits 0,1 set, bar has bit 1,2 set, baz has bit 2,3 set
+        byteString.set("foo", "00000011");
+        byteString.set("bar", "00000110");
+        byteString.set("baz", "00001100");
+
+        // DIFF1 should return bits in (bar OR baz) that are not in foo
+        // foo: 00000011 (bits 0,1 set), bar: 00000110 (bits 1,2 set), baz: 00001100 (bits 2,3 set)
+        // bar OR baz: 00001110
+        // NOT foo AND (bar OR baz): 00001100
+        assertThat(redis.bitopDiff1(key, "foo", "bar", "baz")).isEqualTo(1);
+        assertThat(bitstring.get(key)).isEqualTo("00001100");
+    }
+
+    @Test
+    void bitopAndor() {
+        assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("8.1.240"));
+        // Test ANDOR: X ∧ (Y1 ∨ Y2 ∨ …)
+        // Set up test data: foo has bits 0,1,2 set, bar has bit 1 set, baz has bit 2,3 set
+        byteString.set("foo", "00000111");
+        byteString.set("bar", "00000010");
+        byteString.set("baz", "00001100");
+
+        // ANDOR should return bits in foo that are also in (bar OR baz)
+        // foo: 00000111 (bits 0,1,2 set), bar: 00000010 (bit 1 set), baz: 00001100 (bits 2,3 set)
+        // bar OR baz: 00001110
+        // foo AND (bar OR baz): 00000110
+        assertThat(redis.bitopAndor(key, "foo", "bar", "baz")).isEqualTo(1);
+        assertThat(bitstring.get(key)).isEqualTo("00000110");
+    }
+
+    @Test
+    void bitopOne() {
+        assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("8.1.240"));
+        // Test ONE: members of exactly one of the given keys
+        // Set up test data: foo has bits 0,1 set, bar has bit 1,2 set, baz has bit 2,3 set
+        byteString.set("foo", "00000011");
+        byteString.set("bar", "00000110");
+        byteString.set("baz", "00001100");
+
+        // ONE should return bits that appear in exactly one key
+        // foo: 00000011 (bits 0,1 set), bar: 00000110 (bits 1,2 set), baz: 00001100 (bits 2,3 set)
+        // bit 0: only in foo (1 key) ✓
+        // bit 1: in foo and bar (2 keys) ✗
+        // bit 2: in bar and baz (2 keys) ✗
+        // bit 3: only in baz (1 key) ✓
+        // Result: 00001001
+        assertThat(redis.bitopOne(key, "foo", "bar", "baz")).isEqualTo(1);
+        assertThat(bitstring.get(key)).isEqualTo("00001001");
+    }
+
+    @Test
+    void bitopOneWithTwoKeys() {
+        assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("8.1.240"));
+        // Test ONE with two keys (should be equivalent to XOR)
+        byteString.set("foo", "00000011");
+        byteString.set("bar", "00000110");
+
+        // ONE with two keys should be same as XOR
+        // foo: 00000011 (bits 0,1 set), bar: 00000110 (bits 1,2 set)
+        // XOR result: 00000101 (bits 0,2 set)
+        assertThat(redis.bitopOne(key, "foo", "bar")).isEqualTo(1);
+        assertThat(bitstring.get(key)).isEqualTo("00000101");
+
+        // Verify it's the same as XOR
+        assertThat(redis.bitopXor("xor_result", "foo", "bar")).isEqualTo(1);
+        assertThat(bitstring.get("xor_result")).isEqualTo("00000101");
+    }
+
+    @Test
+    void bitopExtendedComplexPatterns() {
+        assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("8.1.240"));
+        // Test with more complex bit patterns using setbit for precise control
+        // X = 11110000 (bits 4,5,6,7), Y1 = 10101010 (bits 1,3,5,7), Y2 = 01010101 (bits 0,2,4,6), Y3 = 11001100 (bits 2,3,6,7)
+
+        // Set X = 11110000 (bits 4,5,6,7 set)
+        byteString.set("X", "11110000");
+
+        // Set Y1 = 10101010 (bits 1,3,5,7 set)
+        byteString.set("Y1", "10101010");
+
+        // Set Y2 = 01010101 (bits 0,2,4,6 set)
+        byteString.set("Y2", "01010101");
+
+        // Set Y3 = 11001100 (bits 2,3,6,7 set)
+        byteString.set("Y3", "11001100");
+
+        // Test DIFF: X ∧ ¬(Y1 ∨ Y2 ∨ Y3)
+        // Y1 ∨ Y2 ∨ Y3 = all bits 0-7 are set = 11111111
+        // X ∧ ¬(Y1 ∨ Y2 ∨ Y3) = 11110000 ∧ 00000000 = 00000000
+        assertThat(redis.bitopDiff("diff_result", "X", "Y1", "Y2", "Y3")).isEqualTo(1);
+        assertThat(bitstring.get("diff_result")).isEqualTo("00000000");
+
+        // Test DIFF1: ¬X ∧ (Y1 ∨ Y2 ∨ Y3)
+        // ¬X = 00001111, Y1 ∨ Y2 ∨ Y3 = 11111111
+        // ¬X ∧ (Y1 ∨ Y2 ∨ Y3) = 00001111 ∧ 11111111 = 00001111
+        assertThat(redis.bitopDiff1("diff1_result", "X", "Y1", "Y2", "Y3")).isEqualTo(1);
+        assertThat(bitstring.get("diff1_result")).isEqualTo("00001111");
+
+        // Test ANDOR: X ∧ (Y1 ∨ Y2 ∨ Y3)
+        // Y1 ∨ Y2 ∨ Y3 = 11111111, X ∧ (Y1 ∨ Y2 ∨ Y3) = 11110000 ∧ 11111111 = 11110000
+        assertThat(redis.bitopAndor("andor_result", "X", "Y1", "Y2", "Y3")).isEqualTo(1);
+        assertThat(bitstring.get("andor_result")).isEqualTo("11110000");
+
+        // Test ONE: bits set in exactly one bitmap
+        // Bit 0: only in Y2 (1 key) ✓
+        // Bit 1: only in Y1 (1 key) ✓
+        // Bit 2: in Y2 and Y3 (2 keys) ✗
+        // Bit 3: in Y1 and Y3 (2 keys) ✗
+        // Bit 4: in X and Y2 (2 keys) ✗
+        // Bit 5: in X and Y1 (2 keys) ✗
+        // Bit 6: in X, Y2, Y3 (3 keys) ✗
+        // Bit 7: in X, Y1, Y3 (3 keys) ✗
+        // Expected result: 00000011 (bits 0,1 set)
+        assertThat(redis.bitopOne("one_result", "X", "Y1", "Y2", "Y3")).isEqualTo(1);
+        assertThat(bitstring.get("one_result")).isEqualTo("00000011");
+    }
+
+    @Test
+    void bitopTwoOperandsComprehensive() {
+        assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("8.1.240"));
+        // Test with two operands: key1=10101010 (bits 1,3,5,7), key2=11001100 (bits 2,3,6,7)
+
+        // Set key1 = 10101010 (bits 1,3,5,7 set)
+        byteString.set("key1", "10101010");
+
+        // Set key2 = 11001100 (bits 2,3,6,7 set)
+        byteString.set("key2", "11001100");
+
+        // Test DIFF: key1 ∧ ¬key2 = 10101010 ∧ ¬11001100 = 10101010 ∧ 00110011 = 00100010
+        assertThat(redis.bitopDiff("diff2_result", "key1", "key2")).isEqualTo(1);
+        assertThat(bitstring.get("diff2_result")).isEqualTo("00100010");
+
+        // Test ONE with two operands (should be equivalent to XOR)
+        // key1 XOR key2 = 10101010 XOR 11001100 = 01100110
+        assertThat(redis.bitopOne("one2_result", "key1", "key2")).isEqualTo(1);
+        assertThat(bitstring.get("one2_result")).isEqualTo("01100110");
+
+        // Verify ONE equals XOR for two operands
+        assertThat(redis.bitopXor("xor2_result", "key1", "key2")).isEqualTo(1);
+        assertThat(bitstring.get("xor2_result")).isEqualTo("01100110");
+        assertThat(bitstring.get("one2_result")).isEqualTo(bitstring.get("xor2_result"));
+    }
+
+    @Test
+    void bitopEdgeCases() {
+        assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("8.1.240"));
+        // Test edge cases with empty keys and minimum operand requirements
+        // Set nonempty = 11110000 (bits 4,5,6,7 set)
+        byteString.set("nonempty", "11110000");
+
+        // DIFF with empty key should return the first key
+        assertThat(redis.bitopDiff("edge_result", "nonempty", "nonexistent")).isEqualTo(1);
+        assertThat(bitstring.get("edge_result")).isEqualTo("11110000");
+
+        // ONE with single key should return that key
+        assertThat(redis.bitopOne("edge_one_result", "nonempty")).isEqualTo(1);
+        assertThat(bitstring.get("edge_one_result")).isEqualTo("11110000");
+
+        // ANDOR requires at least two keys - test with empty second key
+        // nonempty ∧ (empty) = nonempty ∧ 00000000 = 00000000
+        assertThat(redis.bitopAndor("edge_andor_result", "nonempty", "nonexistent")).isEqualTo(1);
+        assertThat(bitstring.get("edge_andor_result")).isEqualTo("00000000");
+
+        // DIFF1 requires at least two keys - test with empty second key
+        // ¬nonempty ∧ (empty) = ¬11110000 ∧ 00000000 = 00001111 ∧ 00000000 = 00000000
+        assertThat(redis.bitopDiff1("edge_diff1_result", "nonempty", "nonexistent")).isEqualTo(1);
+        assertThat(bitstring.get("edge_diff1_result")).isEqualTo("00000000");
+    }
+
+    @Test
+    void bitopMinimumOperandRequirements() {
+        assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("8.1.240"));
+        // Test minimum operand requirements for each BITOP operation
+        byteString.set("test_key", "00000011");
+
+        // Operations that work with single operand
+        assertThat(redis.bitopOne("single_one", "test_key")).isEqualTo(1);
+        assertThat(bitstring.get("single_one")).isEqualTo("00000011");
+
+        // Note: DIFF1 requires at least two keys, so we test with an empty second key
+        assertThat(redis.bitopDiff1("single_diff1", "test_key", "nonexistent")).isEqualTo(1);
+        assertThat(bitstring.get("single_diff1")).isEqualTo("00000000");
+
+        // Operations that require at least two operands
+        redis.setbit("second_key", 1, 1);
+        redis.setbit("second_key", 2, 1);
+
+        assertThat(redis.bitopDiff("two_diff", "test_key", "second_key")).isEqualTo(1);
+        assertThat(bitstring.get("two_diff")).isEqualTo("00000001");
+
+        assertThat(redis.bitopAndor("two_andor", "test_key", "second_key")).isEqualTo(1);
+        assertThat(bitstring.get("two_andor")).isEqualTo("00000010");
+    }
+
     @Test
     void getbit() {
         assertThat(redis.getbit(key, 0)).isEqualTo(0);