diff --git a/CHANGELOG.md b/CHANGELOG.md index aebe2573..f688ef67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ### Maintenance - Added testing with SSL connection (#1624) - Added testing with SSL authentication using CN (#1627) +- Added testing with TimeSeries NaN values (#1628) - Added support for Redis 8.6 (#1631) ## v3.3.0 (2025-11-24) diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index 68187529..29a38d1f 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -272,7 +272,7 @@ use Predis\Command\Redis\VADD; * @method $this topklist(string $key, bool $withCount = false) * @method $this topkquery(string $key, ...$items) * @method $this topkreserve(string $key, int $topK, int $width = 8, int $depth = 7, float $decay = 0.9) - * @method $this tsadd(string $key, int $timestamp, float $value, ?AddArguments $arguments = null) + * @method $this tsadd(string $key, int $timestamp, string|float $value, ?AddArguments $arguments = null) * @method $this tsalter(string $key, ?TSAlterArguments $arguments = null) * @method $this tscreate(string $key, ?TSCreateArguments $arguments = null) * @method $this tscreaterule(string $sourceKey, string $destKey, string $aggregator, int $bucketDuration, int $alignTimestamp = 0) diff --git a/src/ClientInterface.php b/src/ClientInterface.php index 9abeadb5..bb3424e2 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -283,7 +283,7 @@ use Predis\Response\Status; * @method array topklist(string $key, bool $withCount = false) * @method array topkquery(string $key, ...$items) * @method Status topkreserve(string $key, int $topK, int $width = 8, int $depth = 7, float $decay = 0.9) - * @method int tsadd(string $key, int $timestamp, float $value, ?AddArguments $arguments = null) + * @method int tsadd(string $key, int $timestamp, string|float $value, ?AddArguments $arguments = null) * @method Status tsalter(string $key, ?TSAlterArguments $arguments = null) * @method Status tscreate(string $key, ?TSCreateArguments $arguments = null) * @method Status tscreaterule(string $sourceKey, string $destKey, string $aggregator, int $bucketDuration, int $alignTimestamp = 0) diff --git a/src/Command/Argument/TimeSeries/RangeArguments.php b/src/Command/Argument/TimeSeries/RangeArguments.php index 6828a7d4..224f0603 100644 --- a/src/Command/Argument/TimeSeries/RangeArguments.php +++ b/src/Command/Argument/TimeSeries/RangeArguments.php @@ -14,6 +14,13 @@ namespace Predis\Command\Argument\TimeSeries; class RangeArguments extends CommonArguments { + public const AGG_SUM = 'sum'; + public const AGG_MIN = 'min'; + public const AGG_MAX = 'max'; + public const AGG_COUNT = 'count'; + public const AGG_COUNT_NAN = 'countNan'; + public const AGG_COUNT_ALL = 'countAll'; + /** * Filters samples by a list of specific timestamps. * @@ -57,7 +64,7 @@ class RangeArguments extends CommonArguments /** * Aggregates samples into time buckets. * - * @param string $aggregator + * @param string $aggregator Aggregation type. Check class constants. * @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. diff --git a/tests/Predis/Command/Argument/TimeSeries/RangeArgumentsTest.php b/tests/Predis/Command/Argument/TimeSeries/RangeArgumentsTest.php index 4326ce91..23258455 100644 --- a/tests/Predis/Command/Argument/TimeSeries/RangeArgumentsTest.php +++ b/tests/Predis/Command/Argument/TimeSeries/RangeArgumentsTest.php @@ -73,24 +73,24 @@ class RangeArgumentsTest extends TestCase { return [ 'with default arguments' => [ - ['sum', 1000], - ['AGGREGATION', 'sum', 1000], + [RangeArguments::AGG_SUM, 1000], + ['AGGREGATION', RangeArguments::AGG_SUM, 1000], ], 'with ALIGN modifier' => [ - ['sum', 1000, 10], - ['ALIGN', 10, 'AGGREGATION', 'sum', 1000], + [RangeArguments::AGG_COUNT, 1000, 10], + ['ALIGN', 10, 'AGGREGATION', RangeArguments::AGG_COUNT, 1000], ], 'with BUCKETTIMESTAMP modifier' => [ - ['sum', 1000, 0, 10000], - ['AGGREGATION', 'sum', 1000, 'BUCKETTIMESTAMP', 10000], + [RangeArguments::AGG_MAX, 1000, 0, 10000], + ['AGGREGATION', RangeArguments::AGG_MAX, 1000, 'BUCKETTIMESTAMP', 10000], ], 'with EMPTY modifier' => [ - ['sum', 1000, 0, 0, true], - ['AGGREGATION', 'sum', 1000, 'EMPTY'], + [RangeArguments::AGG_MIN, 1000, 0, 0, true], + ['AGGREGATION', RangeArguments::AGG_MIN, 1000, 'EMPTY'], ], 'with all arguments' => [ - ['sum', 1000, 10, 10000, true], - ['ALIGN', 10, 'AGGREGATION', 'sum', 1000, 'BUCKETTIMESTAMP', 10000, 'EMPTY'], + [RangeArguments::AGG_COUNT_NAN, 1000, 10, 10000, true], + ['ALIGN', 10, 'AGGREGATION', RangeArguments::AGG_COUNT_NAN, 1000, 'BUCKETTIMESTAMP', 10000, 'EMPTY'], ], ]; } diff --git a/tests/Predis/Command/Redis/TimeSeries/TSMRANGE_Test.php b/tests/Predis/Command/Redis/TimeSeries/TSMRANGE_Test.php index ddf7a549..6722f586 100644 --- a/tests/Predis/Command/Redis/TimeSeries/TSMRANGE_Test.php +++ b/tests/Predis/Command/Redis/TimeSeries/TSMRANGE_Test.php @@ -12,8 +12,10 @@ namespace Predis\Command\Redis\TimeSeries; +use Predis\Command\Argument\TimeSeries\CommonArguments; use Predis\Command\Argument\TimeSeries\CreateArguments; use Predis\Command\Argument\TimeSeries\MRangeArguments; +use Predis\Command\Argument\TimeSeries\RangeArguments; use Predis\Command\Redis\PredisCommandTestCase; /** @@ -154,6 +156,76 @@ class TSMRANGE_Test extends PredisCommandTestCase $this->assertEquals($expectedResponse, $redis->tsmrange('-', '+', $mrangeArguments)); } + /** + * @group connected + * @group relay-resp3 + * @return void + * @requiresRedisVersion >= 8.5.0 + */ + public function testAddSamplesIntoFewTimeSeriesWithNaNValues(): void + { + $redis = $this->getClient(); + + $createArguments = (new CreateArguments()) + ->retentionMsecs(60000) + ->duplicatePolicy(CommonArguments::POLICY_MAX) + ->labels('type', 'temperature', 'name', 'A'); + + $this->assertEquals( + 'OK', + $redis->tscreate('temperature:A', $createArguments) + ); + + $createArguments = (new CreateArguments()) + ->retentionMsecs(60000) + ->duplicatePolicy(CommonArguments::POLICY_MAX) + ->labels('type', 'temperature', 'name', 'B'); + + $this->assertEquals( + 'OK', + $redis->tscreate('temperature:B', $createArguments) + ); + + // Add NaN value samples + $this->assertEquals( + [1000, 1001], + $redis->tsmadd( + 'temperature:A', 1000, 'NaN', 'temperature:A', 1001, 27 + ) + ); + + $this->assertEquals( + [1000, 1001], + $redis->tsmadd( + 'temperature:B', 1000, 'NaN', 'temperature:B', 1001, 28 + ) + ); + + // Ensure that we can count all values (included NaN) + $mRangeArguments = (new MRangeArguments()) + ->aggregation(RangeArguments::AGG_COUNT_ALL, 1000) + ->filter('type=temperature'); + + $expectedResponse = [ + ['temperature:A', [], [[1000, 2]]], + ['temperature:B', [], [[1000, 2]]], + ]; + + $this->assertEquals($expectedResponse, $redis->tsmrange(1000, 1001, $mRangeArguments)); + + // Ensure that we can count NaN values only + $mRangeArguments = (new MRangeArguments()) + ->aggregation(RangeArguments::AGG_COUNT_NAN, 1000) + ->filter('type=temperature'); + + $expectedResponse = [ + ['temperature:A', [], [[1000, 1]]], + ['temperature:B', [], [[1000, 1]]], + ]; + + $this->assertEquals($expectedResponse, $redis->tsmrange(1000, 1001, $mRangeArguments)); + } + public function argumentsProvider(): array { return [ diff --git a/tests/Predis/Command/Redis/TimeSeries/TSMREVRANGE_Test.php b/tests/Predis/Command/Redis/TimeSeries/TSMREVRANGE_Test.php index 9bd8a000..4c89a728 100644 --- a/tests/Predis/Command/Redis/TimeSeries/TSMREVRANGE_Test.php +++ b/tests/Predis/Command/Redis/TimeSeries/TSMREVRANGE_Test.php @@ -12,8 +12,10 @@ namespace Predis\Command\Redis\TimeSeries; +use Predis\Command\Argument\TimeSeries\CommonArguments; use Predis\Command\Argument\TimeSeries\CreateArguments; use Predis\Command\Argument\TimeSeries\MRangeArguments; +use Predis\Command\Argument\TimeSeries\RangeArguments; use Predis\Command\Redis\PredisCommandTestCase; /** @@ -154,6 +156,76 @@ class TSMREVRANGE_Test extends PredisCommandTestCase $this->assertEquals($expectedResponse, $redis->tsmrevrange('-', '+', $mrangeArguments)); } + /** + * @group connected + * @group relay-resp3 + * @return void + * @requiresRedisVersion >= 8.5.0 + */ + public function testAddSamplesIntoFewTimeSeriesWithNaNValues(): void + { + $redis = $this->getClient(); + + $createArguments = (new CreateArguments()) + ->retentionMsecs(60000) + ->duplicatePolicy(CommonArguments::POLICY_MAX) + ->labels('type', 'temperature', 'name', 'A'); + + $this->assertEquals( + 'OK', + $redis->tscreate('temperature:A', $createArguments) + ); + + $createArguments = (new CreateArguments()) + ->retentionMsecs(60000) + ->duplicatePolicy(CommonArguments::POLICY_MAX) + ->labels('type', 'temperature', 'name', 'B'); + + $this->assertEquals( + 'OK', + $redis->tscreate('temperature:B', $createArguments) + ); + + // Add NaN value samples + $this->assertEquals( + [1000, 1001], + $redis->tsmadd( + 'temperature:A', 1000, 'NaN', 'temperature:A', 1001, 27 + ) + ); + + $this->assertEquals( + [1000, 1001], + $redis->tsmadd( + 'temperature:B', 1000, 'NaN', 'temperature:B', 1001, 28 + ) + ); + + // Ensure that we can count all values (included NaN) + $mRangeArguments = (new MRangeArguments()) + ->aggregation(RangeArguments::AGG_COUNT_ALL, 1000) + ->filter('type=temperature'); + + $expectedResponse = [ + ['temperature:A', [], [[1000, 2]]], + ['temperature:B', [], [[1000, 2]]], + ]; + + $this->assertEquals($expectedResponse, $redis->tsmrevrange(1000, 1001, $mRangeArguments)); + + // Ensure that we can count NaN values only + $mRangeArguments = (new MRangeArguments()) + ->aggregation(RangeArguments::AGG_COUNT_NAN, 1000) + ->filter('type=temperature'); + + $expectedResponse = [ + ['temperature:A', [], [[1000, 1]]], + ['temperature:B', [], [[1000, 1]]], + ]; + + $this->assertEquals($expectedResponse, $redis->tsmrevrange(1000, 1001, $mRangeArguments)); + } + public function argumentsProvider(): array { return [ diff --git a/tests/Predis/Command/Redis/TimeSeries/TSRANGE_Test.php b/tests/Predis/Command/Redis/TimeSeries/TSRANGE_Test.php index f34ffc7f..a452f396 100644 --- a/tests/Predis/Command/Redis/TimeSeries/TSRANGE_Test.php +++ b/tests/Predis/Command/Redis/TimeSeries/TSRANGE_Test.php @@ -12,6 +12,7 @@ namespace Predis\Command\Redis\TimeSeries; +use Predis\Command\Argument\TimeSeries\CommonArguments; use Predis\Command\Argument\TimeSeries\CreateArguments; use Predis\Command\Argument\TimeSeries\RangeArguments; use Predis\Command\PrefixableCommand; @@ -126,6 +127,63 @@ class TSRANGE_Test extends PredisCommandTestCase ); } + /** + * @group connected + * @group relay-resp3 + * @return void + * @requiresRedisVersion >= 8.5.0 + */ + public function testAddSampleIntoTimeSeriesWithNaNValues(): void + { + $redis = $this->getClient(); + + $createArguments = (new CreateArguments()) + ->retentionMsecs(31536000000) + ->duplicatePolicy(CommonArguments::POLICY_LAST) + ->ignore(10, 10); + + $this->assertEquals('OK', $redis->tscreate('temperature:2:32', $createArguments)); + + // Add NaN value samples + $this->assertEquals( + 1000, + $redis->tsadd('temperature:2:32', 1000, 'NaN') + ); + + $this->assertEquals( + 1003, + $redis->tsadd('temperature:2:32', 1003, 25) + ); + + $this->assertEquals( + 1005, + $redis->tsadd('temperature:2:32', 1005, 'NaN') + ); + + $this->assertEquals( + 1006, + $redis->tsadd('temperature:2:32', 1006, 'NaN') + ); + + // Ensure that we can count NaN values only + $rangeArguments = (new RangeArguments()) + ->aggregation(RangeArguments::AGG_COUNT_NAN, 1000); + + $this->assertEquals( + [[1000, 3]], + $redis->tsrange('temperature:2:32', 1000, 1006, $rangeArguments) + ); + + // Ensure that we can count all values (included NaN) + $rangeArguments = (new RangeArguments()) + ->aggregation(RangeArguments::AGG_COUNT_ALL, 1000); + + $this->assertEquals( + [[1000, 4]], + $redis->tsrange('temperature:2:32', 1000, 1006, $rangeArguments) + ); + } + /** * @group connected * @group relay-resp3 diff --git a/tests/Predis/Command/Redis/TimeSeries/TSREVRANGE_Test.php b/tests/Predis/Command/Redis/TimeSeries/TSREVRANGE_Test.php index 3d40d1af..cb594d9a 100644 --- a/tests/Predis/Command/Redis/TimeSeries/TSREVRANGE_Test.php +++ b/tests/Predis/Command/Redis/TimeSeries/TSREVRANGE_Test.php @@ -12,6 +12,7 @@ namespace Predis\Command\Redis\TimeSeries; +use Predis\Command\Argument\TimeSeries\CommonArguments; use Predis\Command\Argument\TimeSeries\CreateArguments; use Predis\Command\Argument\TimeSeries\RangeArguments; use Predis\Command\PrefixableCommand; @@ -126,6 +127,63 @@ class TSREVRANGE_Test extends PredisCommandTestCase ); } + /** + * @group connected + * @group relay-resp3 + * @return void + * @requiresRedisVersion >= 8.5.0 + */ + public function testAddSampleIntoTimeSeriesWithNaNValues(): void + { + $redis = $this->getClient(); + + $createArguments = (new CreateArguments()) + ->retentionMsecs(31536000000) + ->duplicatePolicy(CommonArguments::POLICY_LAST) + ->ignore(10, 10); + + $this->assertEquals('OK', $redis->tscreate('temperature:2:32', $createArguments)); + + // Add NaN value samples + $this->assertEquals( + 1000, + $redis->tsadd('temperature:2:32', 1000, 'NaN') + ); + + $this->assertEquals( + 1003, + $redis->tsadd('temperature:2:32', 1003, 25) + ); + + $this->assertEquals( + 1005, + $redis->tsadd('temperature:2:32', 1005, 'NaN') + ); + + $this->assertEquals( + 1006, + $redis->tsadd('temperature:2:32', 1006, 'NaN') + ); + + // Ensure that we can count NaN values only + $rangeArguments = (new RangeArguments()) + ->aggregation(RangeArguments::AGG_COUNT_NAN, 1000); + + $this->assertEquals( + [[1000, 3]], + $redis->tsrevrange('temperature:2:32', 1000, 1006, $rangeArguments) + ); + + // Ensure that we can count all values (included NaN) + $rangeArguments = (new RangeArguments()) + ->aggregation(RangeArguments::AGG_COUNT_ALL, 1000); + + $this->assertEquals( + [[1000, 4]], + $redis->tsrevrange('temperature:2:32', 1000, 1006, $rangeArguments) + ); + } + /** * @group connected * @group relay-resp3