From dcd188d4dd21d4baaff5273251e1d1da60e820c2 Mon Sep 17 00:00:00 2001 From: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Date: Tue, 28 Mar 2023 16:52:00 +0300 Subject: [PATCH] Extended TimeSeries by implementing TS.RANGE command (#1231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added support for new arguments for BITPOS, BITCOUNT commands (#1045) * Added support for new arguments for EXPIRE, EXPIREAT commands (#1046) * Extended core support by implementing SORT_RO command (#1044) * Added support for SORT_RO command * Codestyle fixes * Added command description --------- Co-authored-by: Vladyslav Vildanov * fix deprecated call * Added support for container commands (#1049) * Added support for container commands FUNCTION LOAD, FUNCTION DELETE and FCALL * Changed ContainerInterface and AbstractContainer * Re-implement logic of abstract methods --------- Co-authored-by: Vladyslav Vildanov * Added stream commands to KeyPrefixProcessor (#1051) Co-authored-by: Vladyslav Vildanov * Fix return type of ReplicationInterface::getSlaves (#1111) * Codestyle fixes * Changed return annotation * Added support for TS.RANGE command --------- Co-authored-by: Vladyslav Vildanov Co-authored-by: Till Krüss Co-authored-by: Stephan --- examples/Commands/TimeSeries/ts_range.php | 40 ++++++ src/ClientContextInterface.php | 4 +- src/ClientInterface.php | 2 + .../Argument/TimeSeries/RangeArguments.php | 85 +++++++++++ src/Command/Redis/TimeSeries/TSRANGE.php | 34 +++++ .../TimeSeries/RangeArgumentsTest.php | 97 +++++++++++++ .../Command/Redis/TimeSeries/TSRANGE_Test.php | 134 ++++++++++++++++++ 7 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 examples/Commands/TimeSeries/ts_range.php create mode 100644 src/Command/Argument/TimeSeries/RangeArguments.php create mode 100644 src/Command/Redis/TimeSeries/TSRANGE.php create mode 100644 tests/Predis/Command/Argument/TimeSeries/RangeArgumentsTest.php create mode 100644 tests/Predis/Command/Redis/TimeSeries/TSRANGE_Test.php diff --git a/examples/Commands/TimeSeries/ts_range.php b/examples/Commands/TimeSeries/ts_range.php new file mode 100644 index 00000000..6ac5055b --- /dev/null +++ b/examples/Commands/TimeSeries/ts_range.php @@ -0,0 +1,40 @@ +labels('type', 'temp', 'location', 'TLV'); +$createResponse = $client->tscreate('temp:TLV', $createArguments); + +echo "Time series creation status: {$createResponse}\n"; + +// 2. Add samples into time series +$maddResponse = $client->tsmadd('temp:TLV', 1000, 30, 'temp:TLV', 1010, 35, 'temp:TLV', 1020, 9999, 'temp:TLV', 1030, 40); +$stringResponse = implode(', ', $maddResponse); + +echo "Samples was added with following timestamps: {$stringResponse}\n"; + +// 3. Query samples by values in the given range +$rangeArguments = (new RangeArguments())->filterByValue(-100, 100); +$rangeResponse = $client->tsrange('temp:TLV', '-', '+', $rangeArguments); + +echo "Samples with temperature in range -100 to 100 degrees:\n"; +print_r($rangeResponse); diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index bceea534..4b389da4 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -35,6 +35,7 @@ use Predis\Command\Argument\TimeSeries\GetArguments; use Predis\Command\Argument\TimeSeries\IncrByArguments; use Predis\Command\Argument\TimeSeries\InfoArguments; use Predis\Command\Argument\TimeSeries\MGetArguments; +use Predis\Command\Argument\TimeSeries\RangeArguments; use Predis\Command\CommandInterface; use Predis\Command\Container\FUNCTIONS; use Predis\Command\Container\Json\JSONDEBUG; @@ -250,7 +251,8 @@ use Predis\Command\Container\Search\FTCONFIG; * @method $this tsinfo(string $key, ?InfoArguments $arguments = null) * @method $this tsmadd(mixed ...$keyTimestampValue) * @method $this tsmget(MGetArguments $arguments, string ...$filterExpression) - * @method $this tsqueryindex(string ...$filterExpression)s + * @method $this tsqueryindex(string ...$filterExpression) + * @method $this tsrange(string $key, $fromTimestamp, $toTimestamp, ?RangeArguments $arguments = null) * @method $this zadd($key, array $membersAndScoresDictionary) * @method $this zcard($key) * @method $this zcount($key, $min, $max) diff --git a/src/ClientInterface.php b/src/ClientInterface.php index f2697f08..f66a3ff9 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -35,6 +35,7 @@ use Predis\Command\Argument\TimeSeries\GetArguments; use Predis\Command\Argument\TimeSeries\IncrByArguments; use Predis\Command\Argument\TimeSeries\InfoArguments; use Predis\Command\Argument\TimeSeries\MGetArguments; +use Predis\Command\Argument\TimeSeries\RangeArguments; use Predis\Command\CommandInterface; use Predis\Command\Container\FUNCTIONS; use Predis\Command\Container\Json\JSONDEBUG; @@ -261,6 +262,7 @@ use Predis\Response\Status; * @method array tsmadd(mixed ...$keyTimestampValue) * @method array tsmget(MGetArguments $arguments, string ...$filterExpression) * @method array tsqueryindex(string ...$filterExpression) + * @method array tsrange(string $key, $fromTimestamp, $toTimestamp, ?RangeArguments $arguments = null) * @method string xadd(string $key, array $dictionary, string $id = '*', array $options = null) * @method int xdel(string $key, string ...$id) * @method int xlen(string $key) diff --git a/src/Command/Argument/TimeSeries/RangeArguments.php b/src/Command/Argument/TimeSeries/RangeArguments.php new file mode 100644 index 00000000..00d3772d --- /dev/null +++ b/src/Command/Argument/TimeSeries/RangeArguments.php @@ -0,0 +1,85 @@ +arguments, 'FILTER_BY_TS', ...$ts); + + return $this; + } + + /** + * Filters samples by minimum and maximum values. + * + * @param int $min + * @param int $max + * @return $this + */ + public function filterByValue(int $min, int $max): self + { + array_push($this->arguments, 'FILTER_BY_VALUE', $min, $max); + + return $this; + } + + /** + * Limits the number of returned samples. + * + * @param int $count + * @return $this + */ + public function count(int $count): self + { + array_push($this->arguments, 'COUNT', $count); + + return $this; + } + + /** + * Aggregates samples into time buckets. + * + * @param string $aggregator + * @param int $bucketDuration Is duration of each bucket, in milliseconds. + * @param int $align It controls the time bucket timestamps by changing the reference timestamp on which a bucket is defined. + * @param int $bucketTimestamp Controls how bucket timestamps are reported. + * @param bool $empty Is a flag, which, when specified, reports aggregations also for empty buckets. + * @return $this + */ + public function aggregation(string $aggregator, int $bucketDuration, int $align = 0, int $bucketTimestamp = 0, bool $empty = false): self + { + if ($align > 0) { + array_push($this->arguments, 'ALIGN', $align); + } + + array_push($this->arguments, 'AGGREGATION', $aggregator, $bucketDuration); + + if ($bucketTimestamp > 0) { + array_push($this->arguments, 'BUCKETTIMESTAMP', $bucketTimestamp); + } + + if (true === $empty) { + $this->arguments[] = 'EMPTY'; + } + + return $this; + } +} diff --git a/src/Command/Redis/TimeSeries/TSRANGE.php b/src/Command/Redis/TimeSeries/TSRANGE.php new file mode 100644 index 00000000..cfbfb207 --- /dev/null +++ b/src/Command/Redis/TimeSeries/TSRANGE.php @@ -0,0 +1,34 @@ +toArray() : []; + + parent::setArguments(array_merge( + [$key, $fromTimestamp, $toTimestamp], + $commandArguments + )); + } +} diff --git a/tests/Predis/Command/Argument/TimeSeries/RangeArgumentsTest.php b/tests/Predis/Command/Argument/TimeSeries/RangeArgumentsTest.php new file mode 100644 index 00000000..5f36be38 --- /dev/null +++ b/tests/Predis/Command/Argument/TimeSeries/RangeArgumentsTest.php @@ -0,0 +1,97 @@ +arguments = new RangeArguments(); + } + + /** + * @return void + */ + public function testCreatesArgumentsWithFilterByTsModifier(): void + { + $this->arguments->filterByTs(1000, 1001); + + $this->assertSame(['FILTER_BY_TS', 1000, 1001], $this->arguments->toArray()); + } + + /** + * @return void + */ + public function testCreatesArgumentsWithFilterByValueModifier(): void + { + $this->arguments->filterByValue(1000, 1001); + + $this->assertSame(['FILTER_BY_VALUE', 1000, 1001], $this->arguments->toArray()); + } + + /** + * @return void + */ + public function testCreatesArgumentsWithCountModifier(): void + { + $this->arguments->count(1000); + + $this->assertSame(['COUNT', 1000], $this->arguments->toArray()); + } + + /** + * @dataProvider aggregatorProvider + * @param array $arguments + * @param array $expectedResponse + * @return void + */ + public function testCreatesArgumentsWithAggregatorModifier(array $arguments, array $expectedResponse): void + { + $this->arguments->aggregation(...$arguments); + + $this->assertSame($expectedResponse, $this->arguments->toArray()); + } + + public function aggregatorProvider(): array + { + return [ + 'with default arguments' => [ + ['sum', 1000], + ['AGGREGATION', 'sum', 1000], + ], + 'with ALIGN modifier' => [ + ['sum', 1000, 10], + ['ALIGN', 10, 'AGGREGATION', 'sum', 1000], + ], + 'with BUCKETTIMESTAMP modifier' => [ + ['sum', 1000, 0, 10000], + ['AGGREGATION', 'sum', 1000, 'BUCKETTIMESTAMP', 10000], + ], + 'with EMPTY modifier' => [ + ['sum', 1000, 0, 0, true], + ['AGGREGATION', 'sum', 1000, 'EMPTY'], + ], + 'with all arguments' => [ + ['sum', 1000, 10, 10000, true], + ['ALIGN', 10, 'AGGREGATION', 'sum', 1000, 'BUCKETTIMESTAMP', 10000, 'EMPTY'], + ], + ]; + } +} diff --git a/tests/Predis/Command/Redis/TimeSeries/TSRANGE_Test.php b/tests/Predis/Command/Redis/TimeSeries/TSRANGE_Test.php new file mode 100644 index 00000000..ccbe69a2 --- /dev/null +++ b/tests/Predis/Command/Redis/TimeSeries/TSRANGE_Test.php @@ -0,0 +1,134 @@ +getCommand(); + $command->setArguments($actualArguments); + + $this->assertSameValues($expectedArguments, $command->getArguments()); + } + + /** + * @group connected + * @return void + * @requiresRedisTimeSeriesVersion >= 1.0.0 + */ + public function testReturnsQueriedRangeInForwardDirectionFromGivenTimeSeries(): void + { + $redis = $this->getClient(); + + $createArguments = (new CreateArguments())->labels('type', 'temp', 'location', 'TLV'); + $this->assertEquals('OK', $redis->tscreate('temp:TLV', $createArguments)); + + $this->assertSame( + [1000, 1010, 1020, 1030], + $redis->tsmadd('temp:TLV', 1000, 30, 'temp:TLV', 1010, 35, 'temp:TLV', 1020, 9999, 'temp:TLV', 1030, 40) + ); + + $rangeArguments = (new RangeArguments())->filterByValue(-100, 100); + $this->assertEquals( + [[1000, '30'], [1010, '35'], [1030, '40']], + $redis->tsrange('temp:TLV', '-', '+', $rangeArguments) + ); + } + + /** + * @group connected + * @return void + * @requiresRedisTimeSeriesVersion >= 1.0.0 + */ + public function testThrowsExceptionOnNonExistingKey(): void + { + $redis = $this->getClient(); + + $this->expectException(ServerException::class); + $this->expectExceptionMessage('ERR TSDB: the key does not exist'); + + $redis->tsrange('non_existing_key', 1000, 1000); + } + + public function argumentsProvider(): array + { + return [ + 'with default arguments' => [ + ['key', 10000, 10001], + ['key', 10000, 10001], + ], + 'with LATEST modifier' => [ + ['key', 10000, 10001, (new RangeArguments())->latest()], + ['key', 10000, 10001, 'LATEST'], + ], + 'with FILTER_BY_TS modifier' => [ + ['key', 10000, 10001, (new RangeArguments())->filterByTs(1000, 1001)], + ['key', 10000, 10001, 'FILTER_BY_TS', 1000, 1001], + ], + 'with FILTER_BY_VALUE modifier' => [ + ['key', 10000, 10001, (new RangeArguments())->filterByValue(1000, 1001)], + ['key', 10000, 10001, 'FILTER_BY_VALUE', 1000, 1001], + ], + 'with COUNT modifier' => [ + ['key', 10000, 10001, (new RangeArguments())->count(100)], + ['key', 10000, 10001, 'COUNT', 100], + ], + 'with AGGREGATION modifier - default arguments' => [ + ['key', 10000, 10001, (new RangeArguments())->aggregation('sum', 100)], + ['key', 10000, 10001, 'AGGREGATION', 'sum', 100], + ], + 'with AGGREGATION modifier - with ALIGN' => [ + ['key', 10000, 10001, (new RangeArguments())->aggregation('sum', 100, 100)], + ['key', 10000, 10001, 'ALIGN', 100, 'AGGREGATION', 'sum', 100], + ], + 'with AGGREGATION modifier - with BUCKETTIMESTAMP' => [ + ['key', 10000, 10001, (new RangeArguments())->aggregation('sum', 100, 0, 1000)], + ['key', 10000, 10001, 'AGGREGATION', 'sum', 100, 'BUCKETTIMESTAMP', 1000], + ], + 'with AGGREGATION modifier - with EMPTY' => [ + ['key', 10000, 10001, (new RangeArguments())->aggregation('sum', 100, 0, 0, true)], + ['key', 10000, 10001, 'AGGREGATION', 'sum', 100, 'EMPTY'], + ], + 'with all modifiers' => [ + ['key', 10000, 10001, (new RangeArguments())->latest()->filterByTs(1000, 1001)->filterByValue(1000, 1001)->count(100)->aggregation('sum', 100)], + ['key', 10000, 10001, 'LATEST', 'FILTER_BY_TS', 1000, 1001, 'FILTER_BY_VALUE', 1000, 1001, 'COUNT', 100, 'AGGREGATION', 'sum', 100], + ], + ]; + } +}