From 5707784900f7d6d9512b9225327a130c955b487d Mon Sep 17 00:00:00 2001 From: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Date: Thu, 26 Jan 2023 15:01:56 +0200 Subject: [PATCH] Extended Bloom Filter support by implementing BF.INSERT command (#891) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added CommandResolver, moved resolve command logic there, added ClientConfiguration object * Fixed test, added dependecies * Added resolved command to commands array * Used aggregation approach for modules * Added decorator to check Redis JSON module version * Added support for JSON.SET and JSON.GET commands * Added separate workflow for redis-stack tests * Changed docker imange name to correct one * Fixed indentation * Added test coverage for JSON.GET command * Changed module version resolving using annotations mapping * Re-written CommandResolver test * Update ClientInterface.php * Changes to CI, readme, removed unused modules from configuration * Fixed build badge URL * Refactored annotation check to be generic for each module * Added support for BF.ADD and BF.EXISTS commands * Added support for BF.INFO command * Fixed arguments data provider * Fixed bug with incorrect tests skip * Added support for BF.RESERVE command * Added command description * Added support for BF.INSERT command Co-authored-by: Vladyslav Vildanov Co-authored-by: Till Krüss --- src/ClientContextInterface.php | 1 + src/ClientInterface.php | 1 + src/Command/Redis/BloomFilter/BFINSERT.php | 68 +++++ src/Command/Traits/BloomFilters/Capacity.php | 57 ++++ src/Command/Traits/BloomFilters/Error.php | 57 ++++ src/Command/Traits/BloomFilters/Items.php | 45 +++ .../Redis/BloomFilter/BFINSERT_Test.php | 272 ++++++++++++++++++ .../Traits/BloomFilters/CapacityTest.php | 86 ++++++ .../Command/Traits/BloomFilters/ErrorTest.php | 86 ++++++ .../Command/Traits/BloomFilters/ItemsTest.php | 69 +++++ 10 files changed, 742 insertions(+) create mode 100644 src/Command/Redis/BloomFilter/BFINSERT.php create mode 100644 src/Command/Traits/BloomFilters/Capacity.php create mode 100644 src/Command/Traits/BloomFilters/Error.php create mode 100644 src/Command/Traits/BloomFilters/Items.php create mode 100644 tests/Predis/Command/Redis/BloomFilter/BFINSERT_Test.php create mode 100644 tests/Predis/Command/Traits/BloomFilters/CapacityTest.php create mode 100644 tests/Predis/Command/Traits/BloomFilters/ErrorTest.php create mode 100644 tests/Predis/Command/Traits/BloomFilters/ItemsTest.php diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index 16314bef..337db2d3 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -45,6 +45,7 @@ use Predis\Command\CommandInterface; * @method $this bfadd(string $key, $item) * @method $this bfexists(string $key, $item) * @method $this bfinfo(string $key, string $modifier = '') + * @method $this bfinsert(string $key, int $capacity = -1, float $error = -1, int $expansion = -1, bool $noCreate = false, bool $nonScaling = false, string ...$item) * @method $this bfloadchunk(string $key, int $iterator, $data) * @method $this bfmadd(string $key, ...$item) * @method $this bfmexists(string $key, ...$item) diff --git a/src/ClientInterface.php b/src/ClientInterface.php index 20a1e334..63fcf41c 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -54,6 +54,7 @@ use Predis\Response\Status; * @method int bfadd(string $key, $item) * @method int bfexists(string $key, $item) * @method array bfinfo(string $key, string $modifier = '') + * @method array bfinsert(string $key, int $capacity = -1, float $error = -1, int $expansion = -1, bool $noCreate = false, bool $nonScaling = false, string ...$item) * @method Status bfloadchunk(string $key, int $iterator, $data) * @method array bfmadd(string $key, ...$item) * @method array bfmexists(string $key, ...$item) diff --git a/src/Command/Redis/BloomFilter/BFINSERT.php b/src/Command/Redis/BloomFilter/BFINSERT.php new file mode 100644 index 00000000..48f2bc54 --- /dev/null +++ b/src/Command/Redis/BloomFilter/BFINSERT.php @@ -0,0 +1,68 @@ +setItems($arguments); + $arguments = $this->getArguments(); + + $this->setExpansion($arguments); + $arguments = $this->getArguments(); + + $this->setErrorRate($arguments); + $arguments = $this->getArguments(); + + $this->setCapacity($arguments); + $this->filterArguments(); + } +} diff --git a/src/Command/Traits/BloomFilters/Capacity.php b/src/Command/Traits/BloomFilters/Capacity.php new file mode 100644 index 00000000..c0dccc8a --- /dev/null +++ b/src/Command/Traits/BloomFilters/Capacity.php @@ -0,0 +1,57 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$capacityArgumentPositionOffset] === -1) { + array_splice($arguments, static::$capacityArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$capacityArgumentPositionOffset] < 1) { + throw new UnexpectedValueException('Wrong capacity argument value or position offset'); + } + + $argument = $arguments[static::$capacityArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$capacityArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$capacityArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$capacityModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/src/Command/Traits/BloomFilters/Error.php b/src/Command/Traits/BloomFilters/Error.php new file mode 100644 index 00000000..661def80 --- /dev/null +++ b/src/Command/Traits/BloomFilters/Error.php @@ -0,0 +1,57 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$errorArgumentPositionOffset] === -1) { + array_splice($arguments, static::$errorArgumentPositionOffset, 1, [false]); + parent::setArguments($arguments); + + return; + } + + if ($arguments[static::$errorArgumentPositionOffset] < 0) { + throw new UnexpectedValueException('Wrong error argument value or position offset'); + } + + $argument = $arguments[static::$errorArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$errorArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$errorArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$errorModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/src/Command/Traits/BloomFilters/Items.php b/src/Command/Traits/BloomFilters/Items.php new file mode 100644 index 00000000..9d2e4dcf --- /dev/null +++ b/src/Command/Traits/BloomFilters/Items.php @@ -0,0 +1,45 @@ += $argumentsLength) { + parent::setArguments($arguments); + + return; + } + + $argument = $arguments[static::$itemsArgumentPositionOffset]; + $argumentsBefore = array_slice($arguments, 0, static::$itemsArgumentPositionOffset); + $argumentsAfter = array_slice($arguments, static::$itemsArgumentPositionOffset + 1); + + parent::setArguments(array_merge( + $argumentsBefore, + [self::$itemsModifier], + [$argument], + $argumentsAfter + )); + } +} diff --git a/tests/Predis/Command/Redis/BloomFilter/BFINSERT_Test.php b/tests/Predis/Command/Redis/BloomFilter/BFINSERT_Test.php new file mode 100644 index 00000000..8e3ac01e --- /dev/null +++ b/tests/Predis/Command/Redis/BloomFilter/BFINSERT_Test.php @@ -0,0 +1,272 @@ +getCommand(); + $command->setArguments($actualArguments); + + $this->assertSameValues($expectedArguments, $command->getArguments()); + } + + /** + * @group disconnected + */ + public function testParseResponse(): void + { + $this->assertSame(1, $this->getCommand()->parseResponse(1)); + } + + /** + * @group connected + * @dataProvider filtersProvider + * @param array $arguments + * @param string $key + * @param string $modifier + * @param array $expectedInfo + * @param array $expectedResponse + * @return void + * @requiresRedisBfVersion >= 1.0.0 + */ + public function testInsertCreatesBloomFilterWithGivenItems( + array $arguments, + string $key, + string $modifier, + array $expectedInfo, + array $expectedResponse + ): void { + $redis = $this->getClient(); + + $actualResponse = $redis->bfinsert(...$arguments); + + $this->assertSame($expectedResponse, $actualResponse); + $this->assertSame($expectedInfo, $redis->bfinfo($key, $modifier)); + } + + /** + * @group connected + * @return void + * @requiresRedisBfVersion >= 1.0.0 + */ + public function testInsertThrowsExceptionOnNonExistingBloomFilterWithNoCreateModifier(): void + { + $redis = $this->getClient(); + + $this->expectException(ServerException::class); + $this->expectExceptionMessage('ERR not found'); + + $redis->bfinsert('key', -1, -1, -1, true, false, 'item1'); + } + + /** + * @group connected + * @return void + * @requiresRedisBfVersion >= 1.0.0 + */ + public function testInsertAddItemOnlyOnExistingFilterWithNoCreateModifier(): void + { + $redis = $this->getClient(); + + $redis->bfadd('key', 'item1'); + $actualResponse = $redis->bfinsert( + 'key', + -1, + -1, + -1, + false, + false, + 'item2' + ); + + $this->assertSame([1], $actualResponse); + $this->assertSame( + [ + 'Capacity' => 100, + 'Size' => 240, + 'Number of filters' => 1, + 'Number of items inserted' => 2, + 'Expansion rate' => 2, + ], + $redis->bfinfo('key', '') + ); + } + + /** + * @group connected + * @dataProvider unexpectedValuesProvider + * @param array $arguments + * @param string $expectedExceptionMessage + * @return void + * @requiresRedisBfVersion >= 1.0.0 + */ + public function testThrowsExceptionOnUnexpectedValueGiven( + array $arguments, + string $expectedExceptionMessage + ): void { + $redis = $this->getClient(); + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + + $redis->bfinsert(...$arguments); + } + + public function argumentsProvider(): array + { + return [ + 'with default arguments' => [ + ['key', -1, -1, -1, false, false, 'item1'], + ['key', 'ITEMS', 'item1'], + ], + 'with CAPACITY modifier' => [ + ['key', 100, -1, -1, false, false, 'item1'], + ['key', 'CAPACITY', 100, 'ITEMS', 'item1'], + ], + 'with ERROR modifier' => [ + ['key', -1, 0.01, -1, false, false, 'item1'], + ['key', 'ERROR', 0.01, 'ITEMS', 'item1'], + ], + 'with EXPANSION modifier' => [ + ['key', -1, -1, 2, false, false, 'item1'], + ['key', 'EXPANSION', 2, 'ITEMS', 'item1'], + ], + 'with NOCREATE modifier' => [ + ['key', -1, -1, -1, true, false, 'item1'], + ['key', 'NOCREATE', 'ITEMS', 'item1'], + ], + 'with NONSCALING modifier' => [ + ['key', -1, -1, -1, false, true, 'item1'], + ['key', 'NONSCALING', 'ITEMS', 'item1'], + ], + 'with all arguments' => [ + ['key', 100, 0.01, 2, true, true, 'item1', 'item2'], + ['key', 'CAPACITY', 100, 'ERROR', 0.01, 'EXPANSION', 2, 'NOCREATE', 'NONSCALING', 'ITEMS', 'item1', 'item2'], + ], + ]; + } + + public function filtersProvider(): array + { + return [ + 'with default filter' => [ + ['key', -1, -1, -1, false, false, 'item1', 'item2'], + 'key', + '', + [ + 'Capacity' => 100, + 'Size' => 240, + 'Number of filters' => 1, + 'Number of items inserted' => 2, + 'Expansion rate' => 2, + ], + [1, 1], + ], + 'with CAPACITY modifier' => [ + ['key', 120, -1, -1, false, false, 'item1', 'item2'], + 'key', + 'capacity', + [120], + [1, 1], + ], + 'with ERROR modifier' => [ + ['key', -1, 0.01, -1, false, false, 'item1', 'item2'], + 'key', + '', + [ + 'Capacity' => 100, + 'Size' => 240, + 'Number of filters' => 1, + 'Number of items inserted' => 2, + 'Expansion rate' => 2, + ], + [1, 1], + ], + 'with EXPANSION modifier' => [ + ['key', -1, -1, 3, false, false, 'item1', 'item2'], + 'key', + 'expansion', + [3], + [1, 1], + ], + 'with NONSCALING modifier' => [ + ['key', -1, -1, -1, false, true, 'item1', 'item2'], + 'key', + 'expansion', + [null], + [1, 1], + ], + 'with all arguments' => [ + ['key', 120, 0.01, 3, false, false, 'item1', 'item2'], + 'key', + '', + [ + 'Capacity' => 120, + 'Size' => 264, + 'Number of filters' => 1, + 'Number of items inserted' => 2, + 'Expansion rate' => 3, + ], + [1, 1], + ], + ]; + } + + public function unexpectedValuesProvider(): array + { + return [ + 'with wrong CAPACITY' => [ + ['key', -5, -1, -1, false, false, 'item1', 'item2'], + 'Wrong capacity argument value or position offset', + ], + 'with wrong ERROR' => [ + ['key', -1, -5, -1, false, false, 'item1', 'item2'], + 'Wrong error argument value or position offset', + ], + 'with wrong EXPANSION' => [ + ['key', -1, -1, -5, false, false, 'item1', 'item2'], + 'Wrong expansion argument value or position offset', + ], + ]; + } +} diff --git a/tests/Predis/Command/Traits/BloomFilters/CapacityTest.php b/tests/Predis/Command/Traits/BloomFilters/CapacityTest.php new file mode 100644 index 00000000..4490c1e1 --- /dev/null +++ b/tests/Predis/Command/Traits/BloomFilters/CapacityTest.php @@ -0,0 +1,86 @@ +testClass = new class() extends RedisCommand { + use Capacity; + + public static $capacityArgumentPositionOffset = 0; + + public function getId() + { + return 'test'; + } + }; + } + + /** + * @dataProvider argumentsProvider + * @param int $offset + * @param array $arguments + * @param array $expectedResponse + * @return void + */ + public function testReturnsCorrectArguments(int $offset, array $arguments, array $expectedResponse): void + { + $this->testClass::$capacityArgumentPositionOffset = $offset; + + $this->testClass->setArguments($arguments); + + $this->assertSameValues($expectedResponse, $this->testClass->getArguments()); + } + + /** + * @return void + */ + public function testThrowsErrorOnUnexpectedValueGiven(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Wrong capacity argument value or position offset'); + + $this->testClass->setArguments([-5]); + } + + public function argumentsProvider(): array + { + return [ + 'with wrong offset' => [ + 1, + [], + [], + ], + 'with default argument' => [ + 0, + [-1], + [false], + ], + 'with non-default argument' => [ + 0, + [10], + ['CAPACITY', 10], + ], + ]; + } +} diff --git a/tests/Predis/Command/Traits/BloomFilters/ErrorTest.php b/tests/Predis/Command/Traits/BloomFilters/ErrorTest.php new file mode 100644 index 00000000..e8b25e6d --- /dev/null +++ b/tests/Predis/Command/Traits/BloomFilters/ErrorTest.php @@ -0,0 +1,86 @@ +testClass = new class() extends RedisCommand { + use Error; + + public static $errorArgumentPositionOffset = 0; + + public function getId() + { + return 'test'; + } + }; + } + + /** + * @dataProvider argumentsProvider + * @param int $offset + * @param array $arguments + * @param array $expectedResponse + * @return void + */ + public function testReturnsCorrectArguments(int $offset, array $arguments, array $expectedResponse): void + { + $this->testClass::$errorArgumentPositionOffset = $offset; + + $this->testClass->setArguments($arguments); + + $this->assertSameValues($expectedResponse, $this->testClass->getArguments()); + } + + /** + * @return void + */ + public function testThrowsErrorOnUnexpectedValueGiven(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Wrong error argument value or position offset'); + + $this->testClass->setArguments([-5]); + } + + public function argumentsProvider(): array + { + return [ + 'with wrong offset' => [ + 1, + [], + [], + ], + 'with default argument' => [ + 0, + [-1], + [false], + ], + 'with non-default argument' => [ + 0, + [0.01], + ['ERROR', 0.01], + ], + ]; + } +} diff --git a/tests/Predis/Command/Traits/BloomFilters/ItemsTest.php b/tests/Predis/Command/Traits/BloomFilters/ItemsTest.php new file mode 100644 index 00000000..bf165ba1 --- /dev/null +++ b/tests/Predis/Command/Traits/BloomFilters/ItemsTest.php @@ -0,0 +1,69 @@ +testClass = new class() extends RedisCommand { + use Items; + + public static $itemsArgumentPositionOffset = 0; + + public function getId() + { + return 'test'; + } + }; + } + + /** + * @dataProvider argumentsProvider + * @param int $offset + * @param array $arguments + * @param array $expectedResponse + * @return void + */ + public function testReturnsCorrectArguments(int $offset, array $arguments, array $expectedResponse): void + { + $this->testClass::$itemsArgumentPositionOffset = $offset; + + $this->testClass->setArguments($arguments); + + $this->assertSameValues($expectedResponse, $this->testClass->getArguments()); + } + + public function argumentsProvider(): array + { + return [ + 'with wrong offset' => [ + 1, + [], + [], + ], + 'with non-default argument' => [ + 0, + ['item1', 'item2'], + ['ITEMS', 'item1', 'item2'], + ], + ]; + } +}