From 2af6a18f6fbb372b31166237f1a0bf9615a73a6d Mon Sep 17 00:00:00 2001 From: adarovadya Date: Sat, 8 Jun 2024 18:00:41 +0300 Subject: [PATCH 1/3] Node: Add ZINTERSTORE command (#1513) * add zinterstore command node * Node: added zinterstore command * Node: added zinterstore command * Node: add ZINTERSTORE command * split test to functions and fix doc * change links to valkey * fix lint errors --------- Co-authored-by: Ubuntu --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 40 ++++++++ node/src/Commands.ts | 44 +++++++++ node/src/Transaction.ts | 28 ++++++ node/tests/RedisClusterClient.test.ts | 1 + node/tests/SharedTests.ts | 135 ++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 8 ++ 7 files changed, 257 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9491973e5..517f223e97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* 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)) * Python: Added OBJECT IDLETIME command ([#1474](https://github.com/aws/glide-for-redis/pull/1474)) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 5bfadd5188..8a5476c0c4 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -11,7 +11,9 @@ import { import * as net from "net"; import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; import { + AggregationType, ExpireOptions, + KeyWeight, RangeByIndex, RangeByLex, RangeByScore, @@ -82,6 +84,7 @@ import { createZAdd, createZCard, createZCount, + createZInterstore, createZPopMax, createZPopMin, createZRange, @@ -1882,6 +1885,43 @@ export class BaseClient { ); } + /** + * Computes the intersection 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 `zinter_withscores`. + * + * When in cluster mode, `destination` and all keys in `keys` must map to the same hash slot. + * + * See https://valkey.io/commands/zinterstore/ for more details. + * + * @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`. + * @returns The number of elements in the resulting sorted set stored at `destination`. + * + * @example + * ```typescript + * // Example usage of zinterstore command with an existing key + * await client.zadd("key1", {"member1": 10.5, "member2": 8.2}) + * await client.zadd("key2", {"member1": 9.5}) + * await client.zinterstore("my_sorted_set", ["key1", "key2"]) // Output: 1 - Indicates that the sorted set "my_sorted_set" contains one element. + * await client.zrange_withscores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 20} - "member1" is now stored in "my_sorted_set" with score of 20. + * await client.zinterstore("my_sorted_set", ["key1", "key2"] , AggregationType.MAX ) // Output: 1 - Indicates that the sorted set "my_sorted_set" contains one element, and it's score is the maximum score between the sets. + * await client.zrange_withscores("my_sorted_set", RangeByIndex(0, -1)) // Output: {'member1': 10.5} - "member1" is now stored in "my_sorted_set" with score of 10.5. + * ``` + */ + public zinterstore( + destination: string, + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, + ): Promise { + return this.createWritePromise( + createZInterstore(destination, keys, aggregationType), + ); + } + /** Returns the length of the string value stored at `key`. * See https://redis.io/commands/strlen/ for more details. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 0cddd74bbe..a4e898d3bf 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -827,6 +827,50 @@ export function createZAdd( return createCommand(RequestType.ZAdd, args); } +/** + * `KeyWeight` - pair of variables represents a weighted key for the `ZINTERSTORE` and `ZUNIONSTORE` sorted sets commands. + */ +export type KeyWeight = [string, number]; +/** + * `AggregationType` - representing aggregation types for `ZINTERSTORE` and `ZUNIONSTORE` sorted set commands. + */ +export type AggregationType = "SUM" | "MIN" | "MAX"; + +/** + * @internal + */ +export function createZInterstore( + destination: string, + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, +): redis_request.Command { + const args = createZCmdStoreArgs(destination, keys, aggregationType); + return createCommand(RequestType.ZInterStore, args); +} + +function createZCmdStoreArgs( + destination: string, + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, +): string[] { + const args: string[] = [destination, keys.length.toString()]; + + if (typeof keys[0] === "string") { + args.push(...(keys as string[])); + } else { + const weightsKeys = keys.map(([key]) => key); + args.push(...(weightsKeys as string[])); + const weights = keys.map(([, weight]) => weight.toString()); + args.push("WEIGHTS", ...weights); + } + + if (aggregationType) { + args.push("AGGREGATE", aggregationType); + } + + return args; +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index e5442e481a..c429743bfc 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -3,8 +3,10 @@ */ import { + AggregationType, ExpireOptions, InfoOptions, + KeyWeight, RangeByIndex, RangeByLex, RangeByScore, @@ -87,6 +89,7 @@ import { createZAdd, createZCard, createZCount, + createZInterstore, createZPopMax, createZPopMin, createZRange, @@ -1036,6 +1039,31 @@ export class BaseTransaction> { ); } + /** + * Computes the intersection 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. + * + * When in cluster mode, `destination` and all keys in `keys` must map to the same hash slot. + * + * See https://valkey.io/commands/zinterstore/ for more details. + * + * @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 zinterstore( + destination: string, + keys: string[] | KeyWeight[], + aggregationType?: AggregationType, + ): T { + return this.addAndReturn( + createZInterstore(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. * diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 4ab7c49e1c..cbe5254b44 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -288,6 +288,7 @@ describe("RedisClusterClient", () => { client.smove("abc", "zxy", "value"), client.renamenx("abc", "zxy"), client.sinter(["abc", "zxy", "lkn"]), + client.zinterstore("abc", ["zxy", "lkn"]), // TODO all rest multi-key commands except ones tested below ]; diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 1844c8c51b..52b43589af 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -1878,6 +1878,141 @@ export function runBaseTests(config: { config.timeout, ); + // Zinterstore command tests + async function zinterstoreWithAggregation(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); + + // Intersection results are aggregated by the MAX score of elements + expect(await client.zinterstore(key3, [key1, key2], "MAX")).toEqual(2); + const zinterstoreMapMax = await client.zrangeWithScores(key3, range); + const expectedMapMax = { + one: 2, + two: 3, + }; + expect(compareMaps(zinterstoreMapMax, expectedMapMax)).toBe(true); + + // Intersection results are aggregated by the MIN score of elements + expect(await client.zinterstore(key3, [key1, key2], "MIN")).toEqual(2); + const zinterstoreMapMin = await client.zrangeWithScores(key3, range); + const expectedMapMin = { + one: 1, + two: 2, + }; + expect(compareMaps(zinterstoreMapMin, expectedMapMin)).toBe(true); + + // Intersection results are aggregated by the SUM score of elements + expect(await client.zinterstore(key3, [key1, key2], "SUM")).toEqual(2); + const zinterstoreMapSum = await client.zrangeWithScores(key3, range); + const expectedMapSum = { + one: 3, + two: 5, + }; + expect(compareMaps(zinterstoreMapSum, expectedMapSum)).toBe(true); + } + + async function zinterstoreBasicTest(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.zinterstore(key3, [key1, key2])).toEqual(2); + const zinterstoreMap = await client.zrangeWithScores(key3, range); + const expectedMap = { + one: 3, + two: 5, + }; + expect(compareMaps(zinterstoreMap, expectedMap)).toBe(true); + } + + async function zinterstoreWithWeightsAndAggregation(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); + + // Scores are multiplied by 2.0 for key1 and key2 during aggregation. + expect( + await client.zinterstore( + key3, + [ + [key1, 2.0], + [key2, 2.0], + ], + "SUM", + ), + ).toEqual(2); + const zinterstoreMapMultiplied = await client.zrangeWithScores( + key3, + range, + ); + const expectedMapMultiplied = { + one: 6, + two: 10, + }; + expect( + compareMaps(zinterstoreMapMultiplied, expectedMapMultiplied), + ).toBe(true); + } + + async function zinterstoreEmptyCases(client: BaseClient) { + const key1 = "{testKey}:1-" + uuidv4(); + const key2 = "{testKey}:2-" + uuidv4(); + + // Non existing key + expect( + await client.zinterstore(key2, [ + key1, + "{testKey}-non_existing_key", + ]), + ).toEqual(0); + + // Empty list check + await expect(client.zinterstore("{xyz}", [])).rejects.toThrow(); + } + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `zinterstore test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + await zinterstoreBasicTest(client); + await zinterstoreWithAggregation(client); + await zinterstoreWithWeightsAndAggregation(client); + await zinterstoreEmptyCases(client); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `type test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 6daeb88fe7..e39831c193 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -229,6 +229,8 @@ export async function transactionTest( const key9 = "{key}" + uuidv4(); const key10 = "{key}" + uuidv4(); const key11 = "{key}" + uuidv4(); // hyper log log + const key12 = "{key}" + uuidv4(); + const key13 = "{key}" + uuidv4(); const field = uuidv4(); const value = uuidv4(); const args: ReturnType[] = []; @@ -349,6 +351,12 @@ export async function transactionTest( args.push(["member2", "member3", "member4", "member5"]); baseTransaction.zrangeWithScores(key8, { start: 0, stop: -1 }); args.push({ member2: 3, member3: 3.5, member4: 4, member5: 5 }); + baseTransaction.zadd(key12, { one: 1, two: 2 }); + args.push(2); + baseTransaction.zadd(key13, { one: 1, two: 2, tree: 3.5 }); + args.push(3); + baseTransaction.zinterstore(key12, [key12, key13]); + args.push(2); baseTransaction.zcount(key8, { value: 2 }, "positiveInfinity"); args.push(4); baseTransaction.zpopmin(key8); From ddc1425f2ffd3b5c65435f89cb1f6536f83bfa58 Mon Sep 17 00:00:00 2001 From: Gilboab <97948000+GilboaAWS@users.noreply.github.com> Date: Sun, 9 Jun 2024 18:07:56 +0300 Subject: [PATCH 2/3] Python: added LMOVE and BLMOVE commands (#1536) Python: added LMOVE and BLMOVE commands Co-authored-by: Shoham Elias --- CHANGELOG.md | 1 + python/python/glide/__init__.py | 3 +- .../glide/async_commands/command_args.py | 16 ++ python/python/glide/async_commands/core.py | 97 +++++++++- .../glide/async_commands/transaction.py | 66 ++++++- python/python/tests/test_async_client.py | 165 +++++++++++++++++- python/python/tests/test_transaction.py | 6 +- 7 files changed, 348 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 517f223e97..cccb3c3026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * Python: Added SINTERCARD command ([#1511](https://github.com/aws/glide-for-redis/pull/1511)) * Python: Added SORT command ([#1439](https://github.com/aws/glide-for-redis/pull/1439)) * Node: Added OBJECT ENCODING command ([#1518](https://github.com/aws/glide-for-redis/pull/1518)) +* Python: Added LMOVE and BLMOVE commands ([#1536](https://github.com/aws/glide-for-redis/pull/1536)) ### Breaking Changes * Node: Update XREAD to return a Map of Map ([#1494](https://github.com/aws/glide-for-redis/pull/1494)) diff --git a/python/python/glide/__init__.py b/python/python/glide/__init__.py index ebd86eca1c..ae938e68d6 100644 --- a/python/python/glide/__init__.py +++ b/python/python/glide/__init__.py @@ -1,6 +1,6 @@ # Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 -from glide.async_commands.command_args import Limit, OrderBy +from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.core import ( ConditionalChange, ExpireOptions, @@ -98,6 +98,7 @@ "json", "LexBoundary", "Limit", + "ListDirection", "RangeByIndex", "RangeByLex", "RangeByScore", diff --git a/python/python/glide/async_commands/command_args.py b/python/python/glide/async_commands/command_args.py index d308ca9ed7..b2e379bff8 100644 --- a/python/python/glide/async_commands/command_args.py +++ b/python/python/glide/async_commands/command_args.py @@ -43,3 +43,19 @@ class OrderBy(Enum): """ DESC: Sort in descending order. """ + + +class ListDirection(Enum): + """ + Enumeration representing element popping or adding direction for List commands. + """ + + LEFT = "LEFT" + """ + LEFT: Represents the option that elements should be popped from or added to the left side of a list. + """ + + RIGHT = "RIGHT" + """ + RIGHT: Represents the option that elements should be popped from or added to the right side of a list. + """ diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index 57d41fbb17..b1284c9b08 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -16,7 +16,7 @@ get_args, ) -from glide.async_commands.command_args import Limit, OrderBy +from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.sorted_set import ( AggregationType, InfBound, @@ -1496,6 +1496,101 @@ async def linsert( ), ) + async def lmove( + self, + source: str, + destination: str, + where_from: ListDirection, + where_to: ListDirection, + ) -> Optional[str]: + """ + Atomically pops and removes the left/right-most element to the list stored at `source` + depending on `where_from`, and pushes the element at the first/last element of the list + stored at `destination` depending on `where_to`. + + When in cluster mode, both `source` and `destination` must map to the same hash slot. + + See https://valkey.io/commands/lmove/ for details. + + Args: + source (str): The key to the source list. + destination (str): The key to the destination list. + where_from (ListDirection): The direction to remove the element from (`ListDirection.LEFT` or `ListDirection.RIGHT`). + where_to (ListDirection): The direction to add the element to (`ListDirection.LEFT` or `ListDirection.RIGHT`). + + Returns: + Optional[str]: The popped element, or None if `source` does not exist. + + Examples: + >>> client.lpush("testKey1", ["two", "one"]) + >>> client.lpush("testKey2", ["four", "three"]) + >>> await client.lmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT) + "one" + >>> updated_array1 = await client.lrange("testKey1", 0, -1) + ["two"] + >>> await client.lrange("testKey2", 0, -1) + ["one", "three", "four"] + + Since: Redis version 6.2.0. + """ + return cast( + Optional[str], + await self._execute_command( + RequestType.LMove, + [source, destination, where_from.value, where_to.value], + ), + ) + + async def blmove( + self, + source: str, + destination: str, + where_from: ListDirection, + where_to: ListDirection, + timeout: float, + ) -> Optional[str]: + """ + Blocks the connection until it pops atomically and removes the left/right-most element to the + list stored at `source` depending on `where_from`, and pushes the element at the first/last element + of the list stored at `destination` depending on `where_to`. + `BLMOVE` is the blocking variant of `LMOVE`. + + Notes: + 1. When in cluster mode, both `source` and `destination` must map to the same hash slot. + 2. `BLMOVE` is a client blocking command, see https://github.com/aws/glide-for-redis/wiki/General-Concepts#blocking-commands for more details and best practices. + + See https://valkey.io/commands/blmove/ for details. + + Args: + source (str): The key to the source list. + destination (str): The key to the destination list. + where_from (ListDirection): The direction to remove the element from (`ListDirection.LEFT` or `ListDirection.RIGHT`). + where_to (ListDirection): The direction to add the element to (`ListDirection.LEFT` or `ListDirection.RIGHT`). + timeout (float): The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. + + Returns: + Optional[str]: The popped element, or None if `source` does not exist or if the operation timed-out. + + Examples: + >>> await client.lpush("testKey1", ["two", "one"]) + >>> await client.lpush("testKey2", ["four", "three"]) + >>> await client.blmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT, 0.1) + "one" + >>> await client.lrange("testKey1", 0, -1) + ["two"] + >>> updated_array2 = await client.lrange("testKey2", 0, -1) + ["one", "three", "four"] + + Since: Redis version 6.2.0. + """ + return cast( + Optional[str], + await self._execute_command( + RequestType.BLMove, + [source, destination, where_from.value, where_to.value, str(timeout)], + ), + ) + async def sadd(self, key: str, members: List[str]) -> int: """ Add specified members to the set stored at `key`. diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index c33497e7ed..c94a4d0b35 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -3,7 +3,7 @@ import threading from typing import List, Mapping, Optional, Tuple, TypeVar, Union -from glide.async_commands.command_args import Limit, OrderBy +from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.core import ( ConditionalChange, ExpireOptions, @@ -931,7 +931,7 @@ def linsert( """ Inserts `element` in the list at `key` either before or after the `pivot`. - See https://redis.io/commands/linsert/ for details. + See https://valkey.io/commands/linsert/ for details. Args: key (str): The key of the list. @@ -949,6 +949,68 @@ def linsert( RequestType.LInsert, [key, position.value, pivot, element] ) + def lmove( + self: TTransaction, + source: str, + destination: str, + where_from: ListDirection, + where_to: ListDirection, + ) -> TTransaction: + """ + Atomically pops and removes the left/right-most element to the list stored at `source` + depending on `where_from`, and pushes the element at the first/last element of the list + stored at `destination` depending on `where_to`. + + See https://valkey.io/commands/lmove/ for details. + + Args: + source (str): The key to the source list. + destination (str): The key to the destination list. + where_from (ListDirection): The direction to remove the element from (`ListDirection.LEFT` or `ListDirection.RIGHT`). + where_to (ListDirection): The direction to add the element to (`ListDirection.LEFT` or `ListDirection.RIGHT`). + + Command response: + Optional[str]: The popped element, or `None` if `source` does not exist. + + Since: Redis version 6.2.0. + """ + return self.append_command( + RequestType.LMove, [source, destination, where_from.value, where_to.value] + ) + + def blmove( + self: TTransaction, + source: str, + destination: str, + where_from: ListDirection, + where_to: ListDirection, + timeout: float, + ) -> TTransaction: + """ + Blocks the connection until it pops atomically and removes the left/right-most element to the + list stored at `source` depending on `where_from`, and pushes the element at the first/last element + of the list stored at `destination` depending on `where_to`. + `blmove` is the blocking variant of `lmove`. + + See https://valkey.io/commands/blmove/ for details. + + Args: + source (str): The key to the source list. + destination (str): The key to the destination list. + where_from (ListDirection): The direction to remove the element from (`ListDirection.LEFT` or `ListDirection.RIGHT`). + where_to (ListDirection): The direction to add the element to (`ListDirection.LEFT` or `ListDirection.RIGHT`). + timeout (float): The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. + + Command response: + Optional[str]: The popped element, or `None` if `source` does not exist or if the operation timed-out. + + Since: Redis version 6.2.0. + """ + return self.append_command( + RequestType.BLMove, + [source, destination, where_from.value, where_to.value, str(timeout)], + ) + def sadd(self: TTransaction, key: str, members: List[str]) -> TTransaction: """ Add specified members to the set stored at `key`. diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index 48abd147fa..164acd4559 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -11,7 +11,7 @@ import pytest from glide import ClosingError, RequestError, Script -from glide.async_commands.command_args import Limit, OrderBy +from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.core import ( ConditionalChange, ExpireOptions, @@ -1063,6 +1063,165 @@ async def test_linsert(self, redis_client: TRedisClient): with pytest.raises(RequestError): await redis_client.linsert(key2, InsertPosition.AFTER, "p", "e") + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_lmove(self, redis_client: TRedisClient): + key1 = "{SameSlot}" + get_random_string(10) + key2 = "{SameSlot}" + get_random_string(10) + + # Initialize the lists + assert await redis_client.lpush(key1, ["2", "1"]) == 2 + assert await redis_client.lpush(key2, ["4", "3"]) == 2 + + # Move from LEFT to LEFT + assert ( + await redis_client.lmove(key1, key2, ListDirection.LEFT, ListDirection.LEFT) + == "1" + ) + assert await redis_client.lrange(key1, 0, -1) == ["2"] + assert await redis_client.lrange(key2, 0, -1) == ["1", "3", "4"] + + # Move from LEFT to RIGHT + assert ( + await redis_client.lmove( + key1, key2, ListDirection.LEFT, ListDirection.RIGHT + ) + == "2" + ) + assert await redis_client.lrange(key1, 0, -1) == [] + assert await redis_client.lrange(key2, 0, -1) == ["1", "3", "4", "2"] + + # Move from RIGHT to LEFT - non-existing destination key + assert ( + await redis_client.lmove( + key2, key1, ListDirection.RIGHT, ListDirection.LEFT + ) + == "2" + ) + assert await redis_client.lrange(key2, 0, -1) == ["1", "3", "4"] + assert await redis_client.lrange(key1, 0, -1) == ["2"] + + # Move from RIGHT to RIGHT + assert ( + await redis_client.lmove( + key2, key1, ListDirection.RIGHT, ListDirection.RIGHT + ) + == "4" + ) + assert await redis_client.lrange(key2, 0, -1) == ["1", "3"] + assert await redis_client.lrange(key1, 0, -1) == ["2", "4"] + + # Non-existing source key + assert ( + await redis_client.lmove( + "{SameSlot}non_existing_key", + key1, + ListDirection.LEFT, + ListDirection.LEFT, + ) + is None + ) + + # Non-list source key + key3 = get_random_string(10) + assert await redis_client.set(key3, "value") == OK + with pytest.raises(RequestError): + await redis_client.lmove(key3, key1, ListDirection.LEFT, ListDirection.LEFT) + + # Non-list destination key + with pytest.raises(RequestError): + await redis_client.lmove(key1, key3, ListDirection.LEFT, ListDirection.LEFT) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_blmove(self, redis_client: TRedisClient): + key1 = "{SameSlot}" + get_random_string(10) + key2 = "{SameSlot}" + get_random_string(10) + + # Initialize the lists + assert await redis_client.lpush(key1, ["2", "1"]) == 2 + assert await redis_client.lpush(key2, ["4", "3"]) == 2 + + # Move from LEFT to LEFT with blocking + assert ( + await redis_client.blmove( + key1, key2, ListDirection.LEFT, ListDirection.LEFT, 0.1 + ) + == "1" + ) + assert await redis_client.lrange(key1, 0, -1) == ["2"] + assert await redis_client.lrange(key2, 0, -1) == ["1", "3", "4"] + + # Move from LEFT to RIGHT with blocking + assert ( + await redis_client.blmove( + key1, key2, ListDirection.LEFT, ListDirection.RIGHT, 0.1 + ) + == "2" + ) + assert await redis_client.lrange(key1, 0, -1) == [] + assert await redis_client.lrange(key2, 0, -1) == ["1", "3", "4", "2"] + + # Move from RIGHT to LEFT non-existing destination with blocking + assert ( + await redis_client.blmove( + key2, key1, ListDirection.RIGHT, ListDirection.LEFT, 0.1 + ) + == "2" + ) + assert await redis_client.lrange(key2, 0, -1) == ["1", "3", "4"] + assert await redis_client.lrange(key1, 0, -1) == ["2"] + + # Move from RIGHT to RIGHT with blocking + assert ( + await redis_client.blmove( + key2, key1, ListDirection.RIGHT, ListDirection.RIGHT, 0.1 + ) + == "4" + ) + assert await redis_client.lrange(key2, 0, -1) == ["1", "3"] + assert await redis_client.lrange(key1, 0, -1) == ["2", "4"] + + # Non-existing source key with blocking + assert ( + await redis_client.blmove( + "{SameSlot}non_existing_key", + key1, + ListDirection.LEFT, + ListDirection.LEFT, + 0.1, + ) + is None + ) + + # Non-list source key with blocking + key3 = get_random_string(10) + assert await redis_client.set(key3, "value") == OK + with pytest.raises(RequestError): + await redis_client.blmove( + key3, key1, ListDirection.LEFT, ListDirection.LEFT, 0.1 + ) + + # Non-list destination key with blocking + with pytest.raises(RequestError): + await redis_client.blmove( + key1, key3, ListDirection.LEFT, ListDirection.LEFT, 0.1 + ) + + # BLMOVE is called against a non-existing key with no timeout, but we wrap the call in an asyncio timeout to + # avoid having the test block forever + async def endless_blmove_call(): + await redis_client.blmove( + "{SameSlot}non_existing_key", + key2, + ListDirection.LEFT, + ListDirection.RIGHT, + 0, + ) + + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(endless_blmove_call(), timeout=3) + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_sadd_srem_smembers_scard(self, redis_client: TRedisClient): @@ -3933,6 +4092,10 @@ async def test_multi_key_command_returns_cross_slot_error( redis_client.zunion(["def", "ghi"]), redis_client.zunion_withscores(["def", "ghi"]), redis_client.sort_store("abc", "zxy"), + redis_client.lmove("abc", "zxy", ListDirection.LEFT, ListDirection.LEFT), + redis_client.blmove( + "abc", "zxy", ListDirection.LEFT, ListDirection.LEFT, 1 + ), ] if not await check_if_server_version_lt(redis_client, "7.0.0"): diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index 3ce0a3acf5..413ff597ff 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -6,7 +6,7 @@ import pytest from glide import RequestError -from glide.async_commands.command_args import Limit, OrderBy +from glide.async_commands.command_args import Limit, ListDirection, OrderBy from glide.async_commands.core import ( GeospatialData, InsertPosition, @@ -182,6 +182,10 @@ async def transaction_test( args.append(OK) transaction.lrange(key5, 0, -1) args.append([value2, value]) + transaction.lmove(key5, key6, ListDirection.LEFT, ListDirection.LEFT) + args.append(value2) + transaction.blmove(key6, key5, ListDirection.LEFT, ListDirection.LEFT, 1) + args.append(value2) transaction.lpop_count(key5, 2) args.append([value2, value]) transaction.linsert(key5, InsertPosition.BEFORE, "non_existing_pivot", "element") From 9caf33d11f660bf2b13f41975b2f44530a3bcf3c Mon Sep 17 00:00:00 2001 From: Guian Gumpac Date: Mon, 10 Jun 2024 16:33:51 +0000 Subject: [PATCH 3/3] Improved bitmap javadocs (#1530) Improved bitmap docs (#344) * Improved bitmap docs * Addressed PR comments * Update java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java * Update java/client/src/main/java/glide/api/models/BaseTransaction.java --------- Co-authored-by: Yury-Fridlyand --- .../api/commands/BitmapBaseCommands.java | 28 ++++++++++--------- .../glide/api/models/BaseTransaction.java | 28 ++++++++++--------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java b/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java index a22ad2ab3f..508cff39ca 100644 --- a/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java @@ -12,6 +12,7 @@ import glide.api.models.commands.bitmap.BitFieldOptions.OffsetMultiplier; import glide.api.models.commands.bitmap.BitmapIndexType; import glide.api.models.commands.bitmap.BitwiseOperation; +import glide.api.models.configuration.ReadFrom; import java.util.concurrent.CompletableFuture; /** @@ -24,7 +25,7 @@ public interface BitmapBaseCommands { /** * Counts the number of set bits (population counting) in a string stored at key. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key for the string to count the set bits of. * @return The number of set bits in the string. Returns zero if the key is missing as it is * treated as an empty string. @@ -44,7 +45,7 @@ public interface BitmapBaseCommands { * -1 being the last element of the list, -2 being the penultimate, and * so on. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key for the string to count the set bits of. * @param start The starting offset byte index. * @param end The ending offset byte index. @@ -67,7 +68,7 @@ public interface BitmapBaseCommands { * so on. * * @since Redis 7.0 and above - * @see redis.io for details. + * @see valkey.io for details. * @param key The key for the string to count the set bits of. * @param start The starting offset. * @param end The ending offset. @@ -92,7 +93,7 @@ public interface BitmapBaseCommands { * non-existent then the bit at offset is set to value and the preceding * bits are set to 0. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param offset The index of the bit to be set. * @param value The bit value to set at offset. The value must be 0 or @@ -110,7 +111,7 @@ public interface BitmapBaseCommands { * Returns the bit value at offset in the string value stored at key. * offset should be greater than or equal to zero. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param offset The index of the bit to return. * @return The bit at offset of the string. Returns zero if the key is empty or if the positive @@ -127,7 +128,7 @@ public interface BitmapBaseCommands { /** * Returns the position of the first bit matching the given bit value. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param bit The bit value to match. The value must be 0 or 1. * @return The position of the first occurrence matching bit in the binary value of @@ -150,7 +151,7 @@ public interface BitmapBaseCommands { * indicating offsets starting at the end of the list, with -1 being the last byte of * the list, -2 being the penultimate, and so on. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param bit The bit value to match. The value must be 0 or 1. * @param start The starting offset. @@ -174,7 +175,7 @@ public interface BitmapBaseCommands { * negative numbers indicating offsets starting at the end of the list, with -1 being * the last byte of the list, -2 being the penultimate, and so on. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param bit The bit value to match. The value must be 0 or 1. * @param start The starting offset. @@ -203,7 +204,7 @@ public interface BitmapBaseCommands { * list, -2 being the penultimate, and so on. * * @since Redis 7.0 and above. - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param bit The bit value to match. The value must be 0 or 1. * @param start The starting offset. @@ -230,7 +231,7 @@ CompletableFuture bitpos( * * @apiNote When in cluster mode, destination and all keys must map to * the same hash slot. - * @see redis.io for details. + * @see valkey.io for details. * @param bitwiseOperation The bitwise operation to perform. * @param destination The key that will store the resulting string. * @param keys The list of keys to perform the bitwise operation on. @@ -251,7 +252,7 @@ CompletableFuture bitop( * Reads or modifies the array of bits representing the string that is held at key * based on the specified subCommands. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param subCommands The subCommands to be performed on the binary value of the string at * key, which could be any of the following: @@ -289,10 +290,11 @@ CompletableFuture bitop( /** * Reads the array of bits representing the string that is held at key based on the - * specified subCommands. + * specified subCommands.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. * * @since Redis 6.0 and above - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param subCommands The GET subCommands to be performed. * @return An array of results from the GET subcommands. diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index 1dfe7fadc1..ad04013935 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -216,6 +216,7 @@ import glide.api.models.commands.stream.StreamAddOptions.StreamAddOptionsBuilder; import glide.api.models.commands.stream.StreamRange; import glide.api.models.commands.stream.StreamTrimOptions; +import glide.api.models.configuration.ReadFrom; import java.util.Arrays; import java.util.Map; import lombok.Getter; @@ -3500,7 +3501,7 @@ public T copy(@NonNull String source, @NonNull String destination) { /** * Counts the number of set bits (population counting) in a string stored at key. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key for the string to count the set bits of. * @return Command Response - The number of set bits in the string. Returns zero if the key is * missing as it is treated as an empty string. @@ -3519,7 +3520,7 @@ public T bitcount(@NonNull String key) { * -1 being the last element of the list, -2 being the penultimate, and * so on. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key for the string to count the set bits of. * @param start The starting byte offset. * @param end The ending byte offset. @@ -3543,7 +3544,7 @@ public T bitcount(@NonNull String key, long start, long end) { * so on. * * @since Redis 7.0 and above - * @see redis.io for details. + * @see valkey.io for details. * @param key The key for the string to count the set bits of. * @param start The starting offset. * @param end The ending offset. @@ -3738,7 +3739,7 @@ public T functionList(@NonNull String libNamePattern, boolean withCode) { * non-existent then the bit at offset is set to value and the preceding * bits are set to 0. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param offset The index of the bit to be set. * @param value The bit value to set at offset. The value must be 0 or @@ -3755,7 +3756,7 @@ public T setbit(@NonNull String key, long offset, long value) { * Returns the bit value at offset in the string value stored at key. * offset should be greater than or equal to zero. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param offset The index of the bit to return. * @return Command Response - The bit at offset of the string. Returns zero if the key is empty or @@ -3837,7 +3838,7 @@ public T blmpop(@NonNull String[] keys, @NonNull ListDirection direction, double /** * Returns the position of the first bit matching the given bit value. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param bit The bit value to match. The value must be 0 or 1. * @return Command Response - The position of the first occurrence matching bit in @@ -3857,7 +3858,7 @@ public T bitpos(@NonNull String key, long bit) { * indicating offsets starting at the end of the list, with -1 being the last byte of * the list, -2 being the penultimate, and so on. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param bit The bit value to match. The value must be 0 or 1. * @param start The starting offset. @@ -3878,7 +3879,7 @@ public T bitpos(@NonNull String key, long bit, long start) { * negative numbers indicating offsets starting at the end of the list, with -1 being * the last byte of the list, -2 being the penultimate, and so on. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param bit The bit value to match. The value must be 0 or 1. * @param start The starting offset. @@ -3905,7 +3906,7 @@ public T bitpos(@NonNull String key, long bit, long start, long end) { * list, -2 being the penultimate, and so on. * * @since Redis 7.0 and above. - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param bit The bit value to match. The value must be 0 or 1. * @param start The starting offset. @@ -3934,7 +3935,7 @@ public T bitpos( * Perform a bitwise operation between multiple keys (containing string values) and store the * result in the destination. * - * @see redis.io for details. + * @see valkey.io for details. * @param bitwiseOperation The bitwise operation to perform. * @param destination The key that will store the resulting string. * @param keys The list of keys to perform the bitwise operation on. @@ -4147,7 +4148,7 @@ public T spopCount(@NonNull String key, long count) { * Reads or modifies the array of bits representing the string that is held at key * based on the specified subCommands. * - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param subCommands The subCommands to be performed on the binary value of the string at * key, which could be any of the following: @@ -4178,10 +4179,11 @@ public T bitfield(@NonNull String key, @NonNull BitFieldSubCommands[] subCommand /** * Reads the array of bits representing the string that is held at key based on the - * specified subCommands. + * specified subCommands.
+ * This command is routed depending on the client's {@link ReadFrom} strategy. * * @since Redis 6.0 and above - * @see redis.io for details. + * @see valkey.io for details. * @param key The key of the string. * @param subCommands The GET subCommands to be performed. * @return Command Response - An array of results from the GET subcommands.