Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node: Add ZUNIONSTORE command #1550

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#### Changes
* Node: Added ZUNIONSTORE command ([#1550](https://github.com/aws/glide-for-redis/pull/1550))
* Node: Added ZINTERSTORE command ([#1513](https://github.com/aws/glide-for-redis/pull/1513))
* Python: Added OBJECT ENCODING command ([#1471](https://github.com/aws/glide-for-redis/pull/1471))
* Python: Added OBJECT FREQ command ([#1472](https://github.com/aws/glide-for-redis/pull/1472))
Expand Down
44 changes: 41 additions & 3 deletions node/src/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,20 @@ import {
createSMove,
createSPop,
createSRem,
createSUnionStore,
createSet,
createStrlen,
createTTL,
createType,
createUnlink,
createXAdd,
createXLen,
createXRead,
createXTrim,
createZAdd,
createZCard,
createZCount,
createZInterCard,
createZInterstore,
createZPopMax,
createZPopMin,
Expand All @@ -98,9 +101,7 @@ import {
createZRemRangeByRank,
createZRemRangeByScore,
createZScore,
createSUnionStore,
createXLen,
createZInterCard,
createZUnionStore,
} from "./Commands";
import {
ClosingError,
Expand Down Expand Up @@ -1973,6 +1974,43 @@ export class BaseClient {
);
}

/**
* Computes the union of sorted sets given by the specified `keys` and stores the result in `destination`.
* If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created.
* To get the result directly, see `zunion_withscores`.
*
* When in cluster mode, `destination` and all keys in `keys` must map to the same hash slot.
*
* See https://valkey.io/commands/zunionstore/ for more details.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* See https://valkey.io/commands/zunionstore/ for more details.
* @see {@link https://valkey.io/commands/zunionstore/|valkey.io} for details.


Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change

* @param destination - The key of the destination sorted set.
* @param keys - The keys of the sorted sets with possible formats:
* string[] - for keys only.
* KeyWeight[] - for weighted keys with score multipliers.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does weighted is clear to the user? I don't know what its mean so i would like to have an explantation in the docs, but if it's normally known to the user it is not necessary.

Copy link
Collaborator

Choose a reason for hiding this comment

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

On Java, we linked to the documentation for WeightedKeys object. Is it easy to jump to KeyWeight object on PyCharm?

* @param aggregationType - Specifies the aggregation strategy to apply when combining the scores of elements. See `AggregationType`.
* @returns The number of elements in the resulting sorted set stored at `destination`.
*
* @example
* ```typescript
* // Example usage of zunionStore command with an existing key
* await client.zadd("key1", {"member1": 10.5, "member2": 8.2})
* await client.zadd("key2", {"member1": 9.5})
* await client.zunionStore("my_sorted_set", ["key1", "key2"]) // Output: 2 - Indicates that the sorted set "my_sorted_set" contains two elements.
* await client.zrangeWithScores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 20, 'member2': 8.2} - "member1" is now stored in "my_sorted_set" with score of 20 and "member2" with score of 8.2.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you add an example of using weighted?

* await client.zunionStore("my_sorted_set", ["key1", "key2"] , AggregationType.MAX ) // Output: 2 - Indicates that the sorted set "my_sorted_set" contains two elements, and each score is the maximum score between the sets.
* await client.zrangeWithScores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 10.5, 'member2': 8.2} - "member1" is now stored in "my_sorted_set" with score of 10.5 and "member2" with score of 8.2.
* ```
*/
public zunionStore(
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should be in lowercase

Suggested change
public zunionStore(
public zunionstore(

Please update examples too

Copy link
Collaborator

Choose a reason for hiding this comment

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

we're not too consistent with the *store naming

Copy link
Collaborator

Choose a reason for hiding this comment

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

It is a single word command name, so in user API we do the same.
In internal API it is in camel case

destination: string,
keys: string[] | KeyWeight[],
aggregationType?: AggregationType,
): Promise<number> {
return this.createWritePromise(
createZUnionStore(destination, keys, aggregationType),
);
}

/** Returns the length of the string value stored at `key`.
* See https://redis.io/commands/strlen/ for more details.
*
Expand Down
12 changes: 12 additions & 0 deletions node/src/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,18 @@ export function createZInterstore(
return createCommand(RequestType.ZInterStore, args);
}

/**
* @internal
*/
export function createZUnionStore(
destination: string,
keys: string[] | KeyWeight[],
aggregationType?: AggregationType,
): redis_request.Command {
const args = createZCmdStoreArgs(destination, keys, aggregationType);
return createCommand(RequestType.ZUnionStore, args);
}

function createZCmdStoreArgs(
destination: string,
keys: string[] | KeyWeight[],
Expand Down
31 changes: 28 additions & 3 deletions node/src/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import {
createSMove,
createSPop,
createSRem,
createSUnionStore,
createSelect,
createSet,
createStrlen,
Expand All @@ -88,11 +89,13 @@ import {
createType,
createUnlink,
createXAdd,
createXLen,
createXRead,
createXTrim,
createZAdd,
createZCard,
createZCount,
createZInterCard,
createZInterstore,
createZPopMax,
createZPopMin,
Expand All @@ -103,9 +106,7 @@ import {
createZRemRangeByRank,
createZRemRangeByScore,
createZScore,
createSUnionStore,
createXLen,
createZInterCard,
createZUnionStore,
} from "./Commands";
import { redis_request } from "./ProtobufMessage";

Expand Down Expand Up @@ -1103,6 +1104,30 @@ export class BaseTransaction<T extends BaseTransaction<T>> {
);
}

/**
* Computes the union of sorted sets given by the specified `keys` and stores the result in `destination`.
* If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created.
* To get the result directly, see `zunion_withscores`.
*
* See https://valkey.io/commands/zunionstore/ for more details.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* See https://valkey.io/commands/zunionstore/ for more details.
* @see {@link https://valkey.io/commands/zunionstore/|valkey.io} for details.


Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change

* @param destination - The key of the destination sorted set.
* @param keys - The keys of the sorted sets with possible formats:
* string[] - for keys only.
* KeyWeight[] - for weighted keys with score multipliers.
* @param aggregationType - Specifies the aggregation strategy to apply when combining the scores of elements. See `AggregationType`.
* Command Response - The number of elements in the resulting sorted set stored at `destination`.
*/
public zunionStore(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
public zunionStore(
public zunionstore(

destination: string,
keys: string[] | KeyWeight[],
aggregationType?: AggregationType,
): T {
return this.addAndReturn(
createZUnionStore(destination, keys, aggregationType),
);
}

/** Returns the string representation of the type of the value stored at `key`.
* See https://redis.io/commands/type/ for more details.
*
Expand Down
1 change: 1 addition & 0 deletions node/tests/RedisClusterClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ describe("RedisClusterClient", () => {
client.renamenx("abc", "zxy"),
client.sinter(["abc", "zxy", "lkn"]),
client.zinterstore("abc", ["zxy", "lkn"]),
client.zunionStore("abc", ["zxy", "lkn"]),
client.sunionstore("abc", ["zxy", "lkn"]),
client.pfcount(["abc", "zxy", "lkn"]),
// TODO all rest multi-key commands except ones tested below
Expand Down
190 changes: 190 additions & 0 deletions node/tests/SharedTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2117,6 +2117,196 @@ export function runBaseTests<Context>(config: {
config.timeout,
);

// ZUnionStore command tests
async function zunionStoreWithMaxAggregation(client: BaseClient) {
const key1 = "{testKey}:1-" + uuidv4();
const key2 = "{testKey}:2-" + uuidv4();
const key3 = "{testKey}:3-" + uuidv4();
const range = {
start: 0,
stop: -1,
};

const membersScores1 = { one: 1.0, two: 2.0 };
const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 };

expect(await client.zadd(key1, membersScores1)).toEqual(2);
expect(await client.zadd(key2, membersScores2)).toEqual(3);

// Union results are aggregated by the MAX score of elements
expect(await client.zunionStore(key3, [key1, key2], "MAX")).toEqual(3);
const zunionstoreMapMax = await client.zrangeWithScores(key3, range);
const expectedMapMax = {
one: 1.5,
two: 2.5,
three: 3.5,
};
expect(zunionstoreMapMax).toEqual(expectedMapMax);
}

async function zunionStoreWithMinAggregation(client: BaseClient) {
const key1 = "{testKey}:1-" + uuidv4();
const key2 = "{testKey}:2-" + uuidv4();
const key3 = "{testKey}:3-" + uuidv4();
const range = {
start: 0,
stop: -1,
};

const membersScores1 = { one: 1.0, two: 2.0 };
const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 };

expect(await client.zadd(key1, membersScores1)).toEqual(2);
expect(await client.zadd(key2, membersScores2)).toEqual(3);

// Union results are aggregated by the MIN score of elements
expect(await client.zunionStore(key3, [key1, key2], "MIN")).toEqual(3);
const zunionstoreMapMin = await client.zrangeWithScores(key3, range);
const expectedMapMin = {
one: 1.0,
two: 2.0,
three: 3.5,
};
expect(zunionstoreMapMin).toEqual(expectedMapMin);
}

async function zunionStoreWithSumAggregation(client: BaseClient) {
const key1 = "{testKey}:1-" + uuidv4();
const key2 = "{testKey}:2-" + uuidv4();
const key3 = "{testKey}:3-" + uuidv4();
const range = {
start: 0,
stop: -1,
};

const membersScores1 = { one: 1.0, two: 2.0 };
const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 };

expect(await client.zadd(key1, membersScores1)).toEqual(2);
expect(await client.zadd(key2, membersScores2)).toEqual(3);

// Union results are aggregated by the SUM score of elements
expect(await client.zunionStore(key3, [key1, key2], "SUM")).toEqual(3);
const zunionstoreMapSum = await client.zrangeWithScores(key3, range);
const expectedMapSum = {
one: 2.5,
two: 4.5,
three: 3.5,
};
expect(zunionstoreMapSum).toEqual(expectedMapSum);
}

async function zunionStoreBasicTest(client: BaseClient) {
const key1 = "{testKey}:1-" + uuidv4();
const key2 = "{testKey}:2-" + uuidv4();
const key3 = "{testKey}:3-" + uuidv4();
const range = {
start: 0,
stop: -1,
};

const membersScores1 = { one: 1.0, two: 2.0 };
const membersScores2 = { one: 2.0, two: 3.0, three: 4.0 };

expect(await client.zadd(key1, membersScores1)).toEqual(2);
expect(await client.zadd(key2, membersScores2)).toEqual(3);

expect(await client.zunionStore(key3, [key1, key2])).toEqual(3);
const zunionstoreMap = await client.zrangeWithScores(key3, range);
const expectedMap = {
one: 3.0,
three: 4.0,
two: 5.0,
};
expect(zunionstoreMap).toEqual(expectedMap);
}

async function zunionStoreWithWeightsAndAggregation(client: BaseClient) {
const key1 = "{testKey}:1-" + uuidv4();
const key2 = "{testKey}:2-" + uuidv4();
const key3 = "{testKey}:3-" + uuidv4();
const range = {
start: 0,
stop: -1,
};
const membersScores1 = { one: 1.0, two: 2.0 };
const membersScores2 = { one: 1.5, two: 2.5, three: 3.5 };

expect(await client.zadd(key1, membersScores1)).toEqual(2);
expect(await client.zadd(key2, membersScores2)).toEqual(3);

// Scores are multiplied by 2.0 for key1 and key2 during aggregation.
expect(
await client.zunionStore(
key3,
[
[key1, 2.0],
[key2, 2.0],
],
"SUM",
),
).toEqual(3);
const zunionstoreMapMultiplied = await client.zrangeWithScores(
key3,
range,
);
const expectedMapMultiplied = {
one: 5.0,
three: 7.0,
two: 9.0,
};
expect(zunionstoreMapMultiplied).toEqual(expectedMapMultiplied);
}

async function zunionStoreEmptyCases(client: BaseClient) {
const key1 = "{testKey}:1-" + uuidv4();
const key2 = "{testKey}:2-" + uuidv4();
const range = {
start: 0,
stop: -1,
};
const membersScores1 = { one: 1.0, two: 2.0 };

expect(await client.zadd(key1, membersScores1)).toEqual(2);

// Non existing key
expect(
await client.zunionStore(key2, [
key1,
"{testKey}-non_existing_key",
]),
).toEqual(2);

const zunionstore_map_nonexistingkey = await client.zrangeWithScores(
key2,
range,
);

const expectedMapMultiplied = {
one: 1.0,
two: 2.0,
};
expect(zunionstore_map_nonexistingkey).toEqual(expectedMapMultiplied);

// Empty list check
await expect(client.zunionStore("{xyz}", [])).rejects.toThrow();
}

it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
`zunionstore test_%p`,
async (protocol) => {
await runTest(async (client: BaseClient) => {
await zunionStoreBasicTest(client);
await zunionStoreWithMaxAggregation(client);
await zunionStoreWithMinAggregation(client);
await zunionStoreWithSumAggregation(client);
await zunionStoreWithWeightsAndAggregation(client);
await zunionStoreEmptyCases(client);
}, protocol);
},
config.timeout,
);

it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
`type test_%p`,
async (protocol) => {
Expand Down
Loading