mirror of
https://github.com/predis/predis.git
synced 2026-09-08 17:36:52 +00:00
Added support for EXCLUDEEMPTY argument for TS.MRANGE and TS.MREVRANGE commands (#1704)
This commit is contained in:
committed by
GitHub
parent
66ce62657d
commit
bc406c7f55
@@ -7,6 +7,7 @@
|
||||
- Added support for new COLLECT reducer for FT.AGGREGATE (#1699)
|
||||
- Added support for `SDIFFCARD` and `SUNIONCARD` command
|
||||
- Added support for `TS.READ` command
|
||||
- Added support for `EXCLUDEEMPTY` argument for `TS.MRANGE` and `TS.MREVRANGE` commands
|
||||
- Added explicit testing for FT.SEARCH timeout policies
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -51,7 +51,8 @@ class MRangeArguments extends RangeArguments
|
||||
* Splits time series into groups, each group contains time series that share the same
|
||||
* value for the provided label name, then aggregates results in each group.
|
||||
*
|
||||
* GROUPBY cannot be combined with multiple aggregators set via aggregation().
|
||||
* GROUPBY cannot be combined with multiple aggregators set via aggregation()
|
||||
* or with excludeEmpty().
|
||||
*
|
||||
* @param string $label
|
||||
* @param string $reducer
|
||||
@@ -69,8 +70,33 @@ class MRangeArguments extends RangeArguments
|
||||
throw new UnexpectedValueException('GROUPBY cannot be combined with multiple aggregators.');
|
||||
}
|
||||
|
||||
if (in_array('EXCLUDEEMPTY', $this->arguments, true)) {
|
||||
throw new UnexpectedValueException('GROUPBY cannot be combined with EXCLUDEEMPTY.');
|
||||
}
|
||||
|
||||
array_push($this->arguments, 'GROUPBY', $label, 'REDUCE', $reducer);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Omits matching time series whose reported samples array is empty
|
||||
* from the command reply.
|
||||
*
|
||||
* EXCLUDEEMPTY cannot be combined with groupBy().
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function excludeEmpty(): self
|
||||
{
|
||||
if (in_array('GROUPBY', $this->arguments, true)) {
|
||||
throw new UnexpectedValueException('EXCLUDEEMPTY cannot be combined with GROUPBY.');
|
||||
}
|
||||
|
||||
if (!in_array('EXCLUDEEMPTY', $this->arguments, true)) {
|
||||
$this->arguments[] = 'EXCLUDEEMPTY';
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,4 +100,67 @@ class MRangeArgumentsTest extends TestCase
|
||||
$this->arguments->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testCreatesArgumentsWithExcludeEmptyModifier(): void
|
||||
{
|
||||
$this->arguments->excludeEmpty();
|
||||
|
||||
$this->assertSame(['EXCLUDEEMPTY'], $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testExcludeEmptyIsEmittedAtMostOnce(): void
|
||||
{
|
||||
$this->arguments->excludeEmpty()->excludeEmpty();
|
||||
|
||||
$this->assertSame(['EXCLUDEEMPTY'], $this->arguments->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testExcludeEmptyComposesWithOtherRangeOptions(): void
|
||||
{
|
||||
$this->arguments
|
||||
->withLabels()
|
||||
->aggregation(RangeArguments::AGG_MIN, 100)
|
||||
->excludeEmpty()
|
||||
->filter('sensor=1');
|
||||
|
||||
$this->assertSame(
|
||||
['WITHLABELS', 'AGGREGATION', RangeArguments::AGG_MIN, 100, 'EXCLUDEEMPTY', 'FILTER', 'sensor=1'],
|
||||
$this->arguments->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testThrowsOnExcludeEmptyWhenGroupByAlreadySet(): void
|
||||
{
|
||||
$this->arguments->groupBy('label', 'reducer');
|
||||
|
||||
$this->expectException(UnexpectedValueException::class);
|
||||
$this->expectExceptionMessage('EXCLUDEEMPTY cannot be combined with GROUPBY.');
|
||||
|
||||
$this->arguments->excludeEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testThrowsOnGroupByWhenExcludeEmptyAlreadySet(): void
|
||||
{
|
||||
$this->arguments->excludeEmpty();
|
||||
|
||||
$this->expectException(UnexpectedValueException::class);
|
||||
$this->expectExceptionMessage('GROUPBY cannot be combined with EXCLUDEEMPTY.');
|
||||
|
||||
$this->arguments->groupBy('label', 'reducer');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
namespace Predis\Command\Redis\TimeSeries;
|
||||
|
||||
use Predis\ClientInterface;
|
||||
use Predis\Command\Argument\TimeSeries\CommonArguments;
|
||||
use Predis\Command\Argument\TimeSeries\CreateArguments;
|
||||
use Predis\Command\Argument\TimeSeries\MRangeArguments;
|
||||
@@ -300,6 +301,135 @@ class TSMRANGE_Test extends PredisCommandTestCase
|
||||
$this->assertSame(1000, $samples[0][0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @group relay-resp3
|
||||
* @return void
|
||||
* @requiresRedisVersion >= 8.10.0
|
||||
*/
|
||||
public function testQueryRangeExcludesEmptySeriesWithExcludeEmptyModifier(): void
|
||||
{
|
||||
$redis = $this->getClient();
|
||||
|
||||
$this->createExcludeEmptyFixture($redis);
|
||||
|
||||
// The matching series "u" is omitted because it has no samples
|
||||
// within the requested range.
|
||||
$expectedResponse = [
|
||||
[
|
||||
's',
|
||||
[['sensor', '1'], ['type', 'demo']],
|
||||
[[100, '100'], [200, '200'], [400, '400']],
|
||||
],
|
||||
[
|
||||
't',
|
||||
[['sensor', '1'], ['type', 'demo']],
|
||||
[[100, '100'], [300, '300'], [400, '400']],
|
||||
],
|
||||
];
|
||||
|
||||
$mrangeArguments = (new MRangeArguments())
|
||||
->withLabels()
|
||||
->excludeEmpty()
|
||||
->filter('sensor=1');
|
||||
|
||||
$this->assertEquals($expectedResponse, $redis->tsmrange('-', 500, $mrangeArguments));
|
||||
|
||||
// Without EXCLUDEEMPTY the empty series "u" is still reported.
|
||||
$mrangeArguments = (new MRangeArguments())->filter('sensor=1');
|
||||
$this->assertCount(3, $redis->tsmrange('-', 500, $mrangeArguments));
|
||||
|
||||
// When every matching series is empty the reply is an empty array.
|
||||
$mrangeArguments = (new MRangeArguments())->excludeEmpty()->filter('sensor=1');
|
||||
$this->assertSame([], $redis->tsmrange(1, 50, $mrangeArguments));
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @group relay-resp3
|
||||
* @return void
|
||||
* @requiresRedisVersion >= 8.10.0
|
||||
*/
|
||||
public function testQueryRangeExcludesEmptySeriesWithExcludeEmptyAndAggregation(): void
|
||||
{
|
||||
$redis = $this->getClient();
|
||||
|
||||
$this->createExcludeEmptyFixture($redis);
|
||||
|
||||
$expectedResponse = [
|
||||
[
|
||||
's',
|
||||
[],
|
||||
[[100, '100'], [200, '200'], [400, '400']],
|
||||
],
|
||||
[
|
||||
't',
|
||||
[],
|
||||
[[100, '100'], [300, '300'], [400, '400']],
|
||||
],
|
||||
];
|
||||
|
||||
$mrangeArguments = (new MRangeArguments())
|
||||
->aggregation('min', 100)
|
||||
->excludeEmpty()
|
||||
->filter('sensor=1');
|
||||
|
||||
$this->assertEquals($expectedResponse, $redis->tsmrange('-', 500, $mrangeArguments));
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @return void
|
||||
* @requiresRedisVersion >= 8.10.0
|
||||
*/
|
||||
public function testQueryRangeExcludesEmptySeriesWithExcludeEmptyModifierResp3(): void
|
||||
{
|
||||
$redis = $this->getResp3Client();
|
||||
|
||||
$this->createExcludeEmptyFixture($redis);
|
||||
|
||||
$expectedResponse = [
|
||||
's' => [
|
||||
['sensor' => '1', 'type' => 'demo'],
|
||||
['aggregators' => []],
|
||||
[[100, 100], [200, 200], [400, 400]],
|
||||
],
|
||||
't' => [
|
||||
['sensor' => '1', 'type' => 'demo'],
|
||||
['aggregators' => []],
|
||||
[[100, 100], [300, 300], [400, 400]],
|
||||
],
|
||||
];
|
||||
|
||||
$mrangeArguments = (new MRangeArguments())
|
||||
->withLabels()
|
||||
->excludeEmpty()
|
||||
->filter('sensor=1');
|
||||
|
||||
$this->assertEquals($expectedResponse, $redis->tsmrange('-', 500, $mrangeArguments));
|
||||
}
|
||||
|
||||
private function createExcludeEmptyFixture(ClientInterface $redis): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
'OK',
|
||||
$redis->tscreate('s', (new CreateArguments())->labels('sensor', 1, 'type', 'demo'))
|
||||
);
|
||||
$this->assertEquals(
|
||||
'OK',
|
||||
$redis->tscreate('t', (new CreateArguments())->labels('sensor', 1, 'type', 'demo'))
|
||||
);
|
||||
$this->assertEquals(
|
||||
'OK',
|
||||
$redis->tscreate('u', (new CreateArguments())->labels('sensor', 1, 'type', 'demo'))
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
[100, 100, 200, 300, 400, 400, 2000],
|
||||
$redis->tsmadd('s', 100, 100, 't', 100, 100, 's', 200, 200, 't', 300, 300, 's', 400, 400, 't', 400, 400, 'u', 2000, 2000)
|
||||
);
|
||||
}
|
||||
|
||||
public function argumentsProvider(): array
|
||||
{
|
||||
return [
|
||||
@@ -323,6 +453,10 @@ class TSMRANGE_Test extends PredisCommandTestCase
|
||||
[1000, 1001, (new MRangeArguments())->withLabels()->filter('filterExpression1', 'filterExpression2')],
|
||||
[1000, 1001, 'WITHLABELS', 'FILTER', 'filterExpression1', 'filterExpression2'],
|
||||
],
|
||||
'with EXCLUDEEMPTY modifier' => [
|
||||
[1000, 1001, (new MRangeArguments())->excludeEmpty()->filter('filterExpression1', 'filterExpression2')],
|
||||
[1000, 1001, 'EXCLUDEEMPTY', 'FILTER', 'filterExpression1', 'filterExpression2'],
|
||||
],
|
||||
'with SELECTED_LABELS modifier' => [
|
||||
[1000, 1001, (new MRangeArguments())->selectedLabels('label1', 'label2')->filter('filterExpression1', 'filterExpression2')],
|
||||
[1000, 1001, 'SELECTED_LABELS', 'label1', 'label2', 'FILTER', 'filterExpression1', 'filterExpression2'],
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
namespace Predis\Command\Redis\TimeSeries;
|
||||
|
||||
use Predis\ClientInterface;
|
||||
use Predis\Command\Argument\TimeSeries\CommonArguments;
|
||||
use Predis\Command\Argument\TimeSeries\CreateArguments;
|
||||
use Predis\Command\Argument\TimeSeries\MRangeArguments;
|
||||
@@ -300,6 +301,99 @@ class TSMREVRANGE_Test extends PredisCommandTestCase
|
||||
$this->assertSame(1000, $samples[0][0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @group relay-resp3
|
||||
* @return void
|
||||
* @requiresRedisVersion >= 8.10.0
|
||||
*/
|
||||
public function testQueryRangeExcludesEmptySeriesWithExcludeEmptyModifier(): void
|
||||
{
|
||||
$redis = $this->getClient();
|
||||
|
||||
$this->createExcludeEmptyFixture($redis);
|
||||
|
||||
// The matching series "u" is omitted because it has no samples within
|
||||
// the requested range; samples are reported in reverse timestamp order.
|
||||
$expectedResponse = [
|
||||
[
|
||||
's',
|
||||
[],
|
||||
[[400, '400'], [200, '200'], [100, '100']],
|
||||
],
|
||||
[
|
||||
't',
|
||||
[],
|
||||
[[400, '400'], [300, '300'], [100, '100']],
|
||||
],
|
||||
];
|
||||
|
||||
$mrangeArguments = (new MRangeArguments())->excludeEmpty()->filter('sensor=1');
|
||||
|
||||
$this->assertEquals($expectedResponse, $redis->tsmrevrange('-', 500, $mrangeArguments));
|
||||
|
||||
// Without EXCLUDEEMPTY the empty series "u" is still reported.
|
||||
$mrangeArguments = (new MRangeArguments())->filter('sensor=1');
|
||||
$this->assertCount(3, $redis->tsmrevrange('-', 500, $mrangeArguments));
|
||||
|
||||
// When every matching series is empty the reply is an empty array.
|
||||
$mrangeArguments = (new MRangeArguments())->excludeEmpty()->filter('sensor=1');
|
||||
$this->assertSame([], $redis->tsmrevrange(1, 50, $mrangeArguments));
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @return void
|
||||
* @requiresRedisVersion >= 8.10.0
|
||||
*/
|
||||
public function testQueryRangeExcludesEmptySeriesWithExcludeEmptyModifierResp3(): void
|
||||
{
|
||||
$redis = $this->getResp3Client();
|
||||
|
||||
$this->createExcludeEmptyFixture($redis);
|
||||
|
||||
$expectedResponse = [
|
||||
's' => [
|
||||
['sensor' => '1', 'type' => 'demo'],
|
||||
['aggregators' => []],
|
||||
[[400, 400], [200, 200], [100, 100]],
|
||||
],
|
||||
't' => [
|
||||
['sensor' => '1', 'type' => 'demo'],
|
||||
['aggregators' => []],
|
||||
[[400, 400], [300, 300], [100, 100]],
|
||||
],
|
||||
];
|
||||
|
||||
$mrangeArguments = (new MRangeArguments())
|
||||
->withLabels()
|
||||
->excludeEmpty()
|
||||
->filter('sensor=1');
|
||||
|
||||
$this->assertEquals($expectedResponse, $redis->tsmrevrange('-', 500, $mrangeArguments));
|
||||
}
|
||||
|
||||
private function createExcludeEmptyFixture(ClientInterface $redis): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
'OK',
|
||||
$redis->tscreate('s', (new CreateArguments())->labels('sensor', 1, 'type', 'demo'))
|
||||
);
|
||||
$this->assertEquals(
|
||||
'OK',
|
||||
$redis->tscreate('t', (new CreateArguments())->labels('sensor', 1, 'type', 'demo'))
|
||||
);
|
||||
$this->assertEquals(
|
||||
'OK',
|
||||
$redis->tscreate('u', (new CreateArguments())->labels('sensor', 1, 'type', 'demo'))
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
[100, 100, 200, 300, 400, 400, 2000],
|
||||
$redis->tsmadd('s', 100, 100, 't', 100, 100, 's', 200, 200, 't', 300, 300, 's', 400, 400, 't', 400, 400, 'u', 2000, 2000)
|
||||
);
|
||||
}
|
||||
|
||||
public function argumentsProvider(): array
|
||||
{
|
||||
return [
|
||||
@@ -323,6 +417,10 @@ class TSMREVRANGE_Test extends PredisCommandTestCase
|
||||
[1000, 1001, (new MRangeArguments())->withLabels()->filter('filterExpression1', 'filterExpression2')],
|
||||
[1000, 1001, 'WITHLABELS', 'FILTER', 'filterExpression1', 'filterExpression2'],
|
||||
],
|
||||
'with EXCLUDEEMPTY modifier' => [
|
||||
[1000, 1001, (new MRangeArguments())->excludeEmpty()->filter('filterExpression1', 'filterExpression2')],
|
||||
[1000, 1001, 'EXCLUDEEMPTY', 'FILTER', 'filterExpression1', 'filterExpression2'],
|
||||
],
|
||||
'with SELECTED_LABELS modifier' => [
|
||||
[1000, 1001, (new MRangeArguments())->selectedLabels('label1', 'label2')->filter('filterExpression1', 'filterExpression2')],
|
||||
[1000, 1001, 'SELECTED_LABELS', 'label1', 'label2', 'FILTER', 'filterExpression1', 'filterExpression2'],
|
||||
|
||||
Reference in New Issue
Block a user