Skip to content

[Redis 8.10] Add SUNIONCARD and SDIFFCARD commands - #4638

Merged
uglide merged 7 commits into
masterfrom
im/sunioncard-sdiffcard
Jul 28, 2026
Merged

uglide merged 7 commits into
masterfrom
im/sunioncard-sdiffcard

Conversation

@uglide

@uglide uglide commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Adds client support for the Redis 8.10 set-cardinality commands (HLD-based):

  • SUNIONCARD numkeys key [key ...] [APPROX] [LIMIT limit] — distinct-element count of a union; APPROX estimates via HyperLogLog (~0.81% standard error, fixed server memory), LIMIT caps the result with server-side early termination, LIMIT 0 = no limit.
  • SDIFFCARD numkeys key [key ...] [LIMIT limit] — count of S0 \ (S1 ∪ … ∪ SN).

Server PRs: redis/redis#14893 (SUNIONCARD), redis/redis#15278 (SDIFFCARD) — both merged, since: 8.10.0.

Why

Counting the members of a derived union/diff previously required SUNIONSTORE+SCARD+DEL (a write — not possible on read-only replicas, pollutes the keyspace) or transferring the whole set to the client. These commands are read-only, single-command, and return a plain integer.

API

String surface (options after keys):

long sunioncard(String... keys);
long sunioncard(List<String> keys);
long sunioncard(String key1, String key2, SUnionCardParams params);
long sunioncard(List<String> keys, SUnionCardParams params);
// sdiffcard: same four overloads with SDiffCardParams

Binary surface — List<byte[]> overloads are impossible (type-erasure clash with List<String> since UnifiedJedis implements both interfaces), so the N-keys-with-options form takes byte[][]:

long sunioncard(byte[]... keys);
long sunioncard(byte[] key1, byte[] key2, SUnionCardParams params);
long sunioncard(byte[][] keys, SUnionCardParams params);
// sdiffcard: same three overloads
  • SUnionCardParams: approx(), limit(long); always emits APPROX before LIMIT (canonical order), each option at most once.
  • SDiffCardParams: limit(long) only — a separate class makes it impossible to send SDIFFCARD APPROX, which the server rejects (ERR syntax error) until its semantics are defined.
  • limit is validated non-negative client-side (IllegalArgumentException); all other errors (WRONGTYPE, invalid numkeys, etc.) propagate from the server unchanged.

Layers intentionally not changed

  • ClusterCommandObjects — no overrides, same as sintercard: keys are declared via numkeys, cluster routing enforces the single-slot constraint and the server returns CROSSSLOT otherwise.
  • docs/ / migration guides — purely additive change, no user-facing config or breaking behavior.

Behavior against older servers

No client-side version checks: on a pre-8.10 server the command is sent and the server's unknown command error is propagated as-is.

Testing

  • Params unit tests (wire order, LIMIT 0, negative-limit rejection, equals/hashCode contract).
  • Mocked delegation tests for every overload in UnifiedJedis and PipeliningBase.
  • Integration tests (RESP2+RESP3) in the unified base (standalone + cluster with hash-tagged keys), CommandObjects-level, and legacy Jedis, gated @SinceRedisVersion("8.9.241") (the 8.10 RC build; 8.10-rc2 image reports redis_version:8.9.241).
  • All assertions verified against a live redislabs/client-libs-test:8.10-rc2 environment first via redis-cli, including edge cases (missing minuend → 0, missing subtrahend ignored, APPROX+LIMIT combination, WRONGTYPE propagation).

🤖 Generated with Claude Code


Note

Low Risk
Additive API and protocol entries only; behavior mirrors existing sintercard patterns with no changes to auth, persistence, or cluster routing beyond new command dispatch.

Overview
Adds Redis 8.10 client support for read-only set cardinality commands SUNIONCARD and SDIFFCARD, so callers can get union/difference sizes without materializing sets or writing temp keys.

SUNIONCARD is wired through CommandObjects, Jedis, UnifiedJedis, and pipeline APIs with string and binary overloads (varargs, List<String>, and params forms). SUnionCardParams encodes APPROX (HLL estimate) and LIMIT in canonical wire order. SDIFFCARD follows the same layering with SDiffCardParams (LIMIT only—no APPROX type so invalid server syntax cannot be sent). Protocol registers the new commands and the APPROX keyword.

Coverage includes param unit tests (wire encoding, validation), mocked delegation for every overload, and integration tests gated to Redis 8.10 RC (standalone, cluster with hash tags, CommandObjects, legacy Jedis).

Reviewed by Cursor Bugbot for commit 2ee876a. Bugbot is set up for automated code reviews on this repo. Configure here.

Add support for the Redis 8.10 set-cardinality commands:

- SUNIONCARD numkeys key [key ...] [APPROX] [LIMIT limit]
- SDIFFCARD numkeys key [key ...] [LIMIT limit]

API shape (options after keys):
- String surface: varargs, List<String>, (key1, key2, params) and
  (List, params) overloads
- Binary surface: varargs, (key1, key2, params) and (byte[][], params)
  overloads (List<byte[]> is impossible due to erasure clash with
  List<String> in UnifiedJedis)
- SUnionCardParams (approx(), limit(long)) emits APPROX before LIMIT;
  SDiffCardParams carries only limit(long) since the server rejects
  SDIFFCARD APPROX

Covers command interfaces, CommandObjects, UnifiedJedis, PipeliningBase
and legacy Jedis, with params unit tests, mocked delegation tests and
integration tests (standalone, cluster, CommandObjects-level, legacy)
gated on @SinceRedisVersion("8.9.241").

Server PRs: redis/redis#14893, redis/redis#15278

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@uglide uglide changed the title Add SUNIONCARD and SDIFFCARD commands [Redis 8.10] Add SUNIONCARD and SDIFFCARD commands Jul 23, 2026
@github-actions

github-actions Bot commented Jul 23, 2026 •

Copy link
Copy Markdown

Test Results

  164 files  + 2    164 suites  +2   30s ⏱️ ±0s
3 246 tests +51  3 240 ✅ +51  6 💤 ±0  0 ❌ ±0 
3 227 runs  +51  3 221 ✅ +51  6 💤 ±0  0 ❌ ±0 

Results for commit 2ee876a. ± Comparison against base commit 56848fd.

♻️ This comment has been updated with latest results.

@uglide

uglide commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@codex

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: 33dd704a11

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ggivo ggivo added this to the 8.0.0 milestone Jul 28, 2026
@ggivo ggivo added the feature label Jul 28, 2026
@uglide
uglide requested a review from ggivo July 28, 2026 14:24

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 20e9698c8b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* Return an approximate cardinality using HyperLogLog (standard error ~0.81%).
* @return SUnionCardParams
*/
public SUnionCardParams approx() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add @SInCE tags to the new parameter APIs

The class-level @since does not annotate the newly introduced public factory and option methods in generated API documentation; sUnionCardParams(), approx(), and limit() are therefore missing the required version metadata, as are the corresponding public methods in SDiffCardParams. Add method-level @since 8.0 tags to these new APIs.

AGENTS.md reference: AGENTS.md:L26-L30

Useful? React with 👍 / 👎.

RPOPLPUSH, BRPOPLPUSH, BLMOVE, LMOVE, LMPOP, BLMPOP, LPUSHX, RPUSHX, // <-- list
SADD, SMEMBERS, SREM, SPOP, SMOVE, SCARD, SRANDMEMBER, SINTER, SINTERSTORE, SUNION, SUNIONSTORE,
SDIFF, SDIFFSTORE, SISMEMBER, SMISMEMBER, SINTERCARD, // <-- set
SDIFF, SDIFFSTORE, SISMEMBER, SMISMEMBER, SINTERCARD, SUNIONCARD, SDIFFCARD, // <-- set

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Register the new cardinality commands as cacheable

When a client is configured with Jedis's default client-side cache, both new read commands always bypass it because DefaultCacheable.DEFAULT_CACHEABLE_COMMANDS was not extended with SUNIONCARD or SDIFFCARD. CacheConnection.executeCommand() consequently records every invocation as non-cacheable and sends it to Redis, unlike the corresponding SUNION and SDIFF operations; add both protocol commands to the default cacheable set so repeated cardinality reads benefit from caching and key-based invalidation.

Useful? React with 👍 / 👎.

commandObjects.sunioncard(Arrays.asList(key1, key2), new SUnionCardParams().approx()));
assertThat(unionCardApprox, equalTo(4L));

Long unionCardBinary = exec(commandObjects.sunioncard(key1.getBytes(), key2.getBytes()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Encode the binary integration keys with SafeEncoder

When the JVM default charset differs from SafeEncoder.DEFAULT_CHARSET, these binary calls address different Redis keys from the preceding string-key setup: the sadd(String, ...) calls encode the keys through SafeEncoder, while String.getBytes() uses the platform default, so the assertions can receive 0 instead of 4 (and the same problem affects the new SDIFFCARD checks below). Use SafeEncoder.encode(key1) and SafeEncoder.encode(key2) for these binary test keys.

AGENTS.md reference: AGENTS.md:L78-L80

Useful? React with 👍 / 👎.

Comment on lines +11 to +12
import static org.junit.jupiter.api.Assertions.*;
import static redis.clients.jedis.util.CommandArgumentsMatchers.*;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace wildcard imports in the parameter tests

The new test class imports both org.junit.jupiter.api.Assertions.* and CommandArgumentsMatchers.*, and SDiffCardParamsTest repeats the same wildcard imports. Replace them with the specific assertion and matcher imports used by each class so the additions comply with the repository's explicit import convention.

AGENTS.md reference: AGENTS.md:L24-L25

Useful? React with 👍 / 👎.

ggivo added 3 commits July 28, 2026 18:01
The aggregate sdiffcard/sunioncard tests mixed all seven public forms
per command, so a failure did not identify the API at fault. One test
per overload, with missing-key coverage added for the List forms;
cluster overrides split to match.
Hardcoded key names on the shared test endpoints can collide across
suites; the registry namespaces them per test and deletes them in
teardown. Cluster overrides hash-tag the per-test prefix so all of a
test's keys share a slot, replacing the literal {.} tags and the
crafted same-slot binary fixture in the new tests.

@ggivo ggivo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

I think we need to address
#4638 (comment)

…card

# Conflicts:
#	src/main/java/redis/clients/jedis/Protocol.java
@uglide
uglide merged commit b258bab into master Jul 28, 2026
14 checks passed
@uglide
uglide deleted the im/sunioncard-sdiffcard branch July 28, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants