From 3964a2576dfa02db0e87b145dcb5ede9160c6a86 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 13 Jul 2026 14:00:56 +0300 Subject: [PATCH 1/2] feat(client): add SUNIONCARD and SDIFFCARD set cardinality commands Add read-only cardinality commands for derived set operations. SUNIONCARD numkeys key [key ...] [APPROX] [LIMIT limit] returns the distinct-element count of the union; supports exact mode (default), optional APPROX (HyperLogLog), and optional LIMIT (LIMIT 0 = no limit), emitted in canonical order APPROX before LIMIT. SDIFFCARD numkeys key [key ...] [LIMIT limit] returns the element count of the difference between the first set and all successive sets. APPROX is not exposed (out of scope until server semantics are defined). Both use pushKeysLength for the numkeys-prefixed key list, are flagged IS_READ_ONLY, return a NumberReply, and propagate server errors as-is. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../client/lib/commands/SDIFFCARD.spec.ts | 34 ++++++++++++ packages/client/lib/commands/SDIFFCARD.ts | 25 +++++++++ .../client/lib/commands/SUNIONCARD.spec.ts | 53 +++++++++++++++++++ packages/client/lib/commands/SUNIONCARD.ts | 31 +++++++++++ packages/client/lib/commands/index.ts | 30 +++++++++++ 5 files changed, 173 insertions(+) create mode 100644 packages/client/lib/commands/SDIFFCARD.spec.ts create mode 100644 packages/client/lib/commands/SDIFFCARD.ts create mode 100644 packages/client/lib/commands/SUNIONCARD.spec.ts create mode 100644 packages/client/lib/commands/SUNIONCARD.ts diff --git a/packages/client/lib/commands/SDIFFCARD.spec.ts b/packages/client/lib/commands/SDIFFCARD.spec.ts new file mode 100644 index 00000000000..3f0f8d7a897 --- /dev/null +++ b/packages/client/lib/commands/SDIFFCARD.spec.ts @@ -0,0 +1,34 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import SDIFFCARD from './SDIFFCARD'; +import { parseArgs } from './generic-transformers'; + +describe('SDIFFCARD', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(SDIFFCARD, ['1', '2']), + ['SDIFFCARD', '2', '1', '2'] + ); + }); + + it('with LIMIT', () => { + assert.deepEqual( + parseArgs(SDIFFCARD, ['1', '2'], { + LIMIT: 3 + }), + ['SDIFFCARD', '2', '1', '2', 'LIMIT', '3'] + ); + }); + }); + + testUtils.testAll('sDiffCard', async client => { + assert.equal( + await client.sDiffCard('key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); +}); diff --git a/packages/client/lib/commands/SDIFFCARD.ts b/packages/client/lib/commands/SDIFFCARD.ts new file mode 100644 index 00000000000..2e07d1b5f6a --- /dev/null +++ b/packages/client/lib/commands/SDIFFCARD.ts @@ -0,0 +1,25 @@ +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; + +/** + * Options for the SDIFFCARD command + * + * @property LIMIT - Cap the returned cardinality at this value; `LIMIT 0` means no limit + */ +export interface SDiffCardOptions { + LIMIT?: number; +} + +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, keys: RedisVariadicArgument, options?: SDiffCardOptions) { + parser.push('SDIFFCARD'); + parser.pushKeysLength(keys); + + if (options?.LIMIT !== undefined) { + parser.push('LIMIT', options.LIMIT.toString()); + } + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SUNIONCARD.spec.ts b/packages/client/lib/commands/SUNIONCARD.spec.ts new file mode 100644 index 00000000000..274548e266b --- /dev/null +++ b/packages/client/lib/commands/SUNIONCARD.spec.ts @@ -0,0 +1,53 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import SUNIONCARD from './SUNIONCARD'; +import { parseArgs } from './generic-transformers'; + +describe('SUNIONCARD', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + parseArgs(SUNIONCARD, ['1', '2']), + ['SUNIONCARD', '2', '1', '2'] + ); + }); + + it('with APPROX', () => { + assert.deepEqual( + parseArgs(SUNIONCARD, ['1', '2'], { + APPROX: true + }), + ['SUNIONCARD', '2', '1', '2', 'APPROX'] + ); + }); + + it('with LIMIT', () => { + assert.deepEqual( + parseArgs(SUNIONCARD, ['1', '2'], { + LIMIT: 3 + }), + ['SUNIONCARD', '2', '1', '2', 'LIMIT', '3'] + ); + }); + + it('with APPROX and LIMIT', () => { + assert.deepEqual( + parseArgs(SUNIONCARD, ['1', '2'], { + APPROX: true, + LIMIT: 3 + }), + ['SUNIONCARD', '2', '1', '2', 'APPROX', 'LIMIT', '3'] + ); + }); + }); + + testUtils.testAll('sUnionCard', async client => { + assert.equal( + await client.sUnionCard('key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); +}); diff --git a/packages/client/lib/commands/SUNIONCARD.ts b/packages/client/lib/commands/SUNIONCARD.ts new file mode 100644 index 00000000000..7bba2c30b42 --- /dev/null +++ b/packages/client/lib/commands/SUNIONCARD.ts @@ -0,0 +1,31 @@ +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; + +/** + * Options for the SUNIONCARD command + * + * @property APPROX - When true, return an approximate cardinality using HyperLogLog + * @property LIMIT - Cap the returned cardinality at this value; `LIMIT 0` means no limit + */ +export interface SUnionCardOptions { + APPROX?: boolean; + LIMIT?: number; +} + +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, keys: RedisVariadicArgument, options?: SUnionCardOptions) { + parser.push('SUNIONCARD'); + parser.pushKeysLength(keys); + + if (options?.APPROX) { + parser.push('APPROX'); + } + + if (options?.LIMIT !== undefined) { + parser.push('LIMIT', options.LIMIT.toString()); + } + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/index.ts b/packages/client/lib/commands/index.ts index 6e0afc385a9..ad8edd25d6a 100644 --- a/packages/client/lib/commands/index.ts +++ b/packages/client/lib/commands/index.ts @@ -276,6 +276,7 @@ import SCRIPT_FLUSH from './SCRIPT_FLUSH'; import SCRIPT_KILL from './SCRIPT_KILL'; import SCRIPT_LOAD from './SCRIPT_LOAD'; import SDIFF from './SDIFF'; +import SDIFFCARD from './SDIFFCARD'; import SDIFFSTORE from './SDIFFSTORE'; import SET from './SET'; import SETBIT from './SETBIT'; @@ -301,6 +302,7 @@ import SREM from './SREM'; import SSCAN from './SSCAN'; import STRLEN from './STRLEN'; import SUNION from './SUNION'; +import SUNIONCARD from './SUNIONCARD'; import SUNIONSTORE from './SUNIONSTORE'; import SWAPDB from './SWAPDB'; import TIME from './TIME'; @@ -4055,6 +4057,20 @@ export default { * @see https://redis.io/commands/sdiff/ */ sDiff: SDIFF, + /** + * Constructs the SDIFFCARD command to return the cardinality of the difference between the first set and all successive sets + * + * @param keys - One or more set keys; the first is the minuend, the rest are subtrahends + * @param options - Options for the SDIFFCARD command + */ + SDIFFCARD, + /** + * Constructs the SDIFFCARD command to return the cardinality of the difference between the first set and all successive sets + * + * @param keys - One or more set keys; the first is the minuend, the rest are subtrahends + * @param options - Options for the SDIFFCARD command + */ + sDiffCard: SDIFFCARD, /** * Constructs the SDIFFSTORE command * @@ -4455,6 +4471,20 @@ export default { * @see https://redis.io/commands/sunion/ */ sUnion: SUNION, + /** + * Constructs the SUNIONCARD command to return the cardinality of the union of all the given sets + * + * @param keys - One or more set keys to union + * @param options - Options for the SUNIONCARD command (APPROX, LIMIT) + */ + SUNIONCARD, + /** + * Constructs the SUNIONCARD command to return the cardinality of the union of all the given sets + * + * @param keys - One or more set keys to union + * @param options - Options for the SUNIONCARD command (APPROX, LIMIT) + */ + sUnionCard: SUNIONCARD, /** * Constructs the SUNIONSTORE command to store the union of multiple sets into a destination set * From 105ce3fafe356355d704bbda6a96477c3d778b04 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 13 Jul 2026 14:46:35 +0300 Subject: [PATCH 2/2] fix(client): gate SUNIONCARD/SDIFFCARD tests to 8.10 and complete JSDoc - Add isVersionGreaterThanHook([8, 10]) so integration tests skip on pre-8.10 servers instead of failing with unknown-command errors. - Add @see links and "Added since Redis 8.10." to the registry entries, matching the surrounding set-command convention. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/client/lib/commands/SDIFFCARD.spec.ts | 2 ++ packages/client/lib/commands/SUNIONCARD.spec.ts | 2 ++ packages/client/lib/commands/index.ts | 8 ++++++++ 3 files changed, 12 insertions(+) diff --git a/packages/client/lib/commands/SDIFFCARD.spec.ts b/packages/client/lib/commands/SDIFFCARD.spec.ts index 3f0f8d7a897..9b3b78906e1 100644 --- a/packages/client/lib/commands/SDIFFCARD.spec.ts +++ b/packages/client/lib/commands/SDIFFCARD.spec.ts @@ -4,6 +4,8 @@ import SDIFFCARD from './SDIFFCARD'; import { parseArgs } from './generic-transformers'; describe('SDIFFCARD', () => { + testUtils.isVersionGreaterThanHook([8, 10]); + describe('transformArguments', () => { it('simple', () => { assert.deepEqual( diff --git a/packages/client/lib/commands/SUNIONCARD.spec.ts b/packages/client/lib/commands/SUNIONCARD.spec.ts index 274548e266b..8f558ba8e64 100644 --- a/packages/client/lib/commands/SUNIONCARD.spec.ts +++ b/packages/client/lib/commands/SUNIONCARD.spec.ts @@ -4,6 +4,8 @@ import SUNIONCARD from './SUNIONCARD'; import { parseArgs } from './generic-transformers'; describe('SUNIONCARD', () => { + testUtils.isVersionGreaterThanHook([8, 10]); + describe('transformArguments', () => { it('simple', () => { assert.deepEqual( diff --git a/packages/client/lib/commands/index.ts b/packages/client/lib/commands/index.ts index ad8edd25d6a..3ae4f4485ff 100644 --- a/packages/client/lib/commands/index.ts +++ b/packages/client/lib/commands/index.ts @@ -4059,16 +4059,20 @@ export default { sDiff: SDIFF, /** * Constructs the SDIFFCARD command to return the cardinality of the difference between the first set and all successive sets + * Added since Redis 8.10. * * @param keys - One or more set keys; the first is the minuend, the rest are subtrahends * @param options - Options for the SDIFFCARD command + * @see https://redis.io/commands/sdiffcard/ */ SDIFFCARD, /** * Constructs the SDIFFCARD command to return the cardinality of the difference between the first set and all successive sets + * Added since Redis 8.10. * * @param keys - One or more set keys; the first is the minuend, the rest are subtrahends * @param options - Options for the SDIFFCARD command + * @see https://redis.io/commands/sdiffcard/ */ sDiffCard: SDIFFCARD, /** @@ -4473,16 +4477,20 @@ export default { sUnion: SUNION, /** * Constructs the SUNIONCARD command to return the cardinality of the union of all the given sets + * Added since Redis 8.10. * * @param keys - One or more set keys to union * @param options - Options for the SUNIONCARD command (APPROX, LIMIT) + * @see https://redis.io/commands/sunioncard/ */ SUNIONCARD, /** * Constructs the SUNIONCARD command to return the cardinality of the union of all the given sets + * Added since Redis 8.10. * * @param keys - One or more set keys to union * @param options - Options for the SUNIONCARD command (APPROX, LIMIT) + * @see https://redis.io/commands/sunioncard/ */ sUnionCard: SUNIONCARD, /**