├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── composer.json
├── composer.lock
├── docker-compose.yml
├── docker
└── xdebug.ini
├── phpunit.xml
├── psalm.xml
├── src
├── AggregationRule.php
├── Client
│ ├── RedisClient.php
│ ├── RedisClientInterface.php
│ └── RedisConnectionParams.php
├── DateTimeUtils.php
├── Exception
│ ├── InvalidAggregationException.php
│ ├── InvalidDuplicatePolicyException.php
│ ├── InvalidFilterOperationException.php
│ ├── RedisAuthenticationException.php
│ ├── RedisClientException.php
│ └── TimestampParsingException.php
├── Filter.php
├── Label.php
├── Metadata.php
├── Sample.php
├── SampleWithLabels.php
└── TimeSeries.php
└── tests
├── Integration
└── IntegrationTest.php
└── Unit
├── AggregationRuleTest.php
├── DateTimeUtilsTest.php
├── FilterTest.php
├── MetadataTest.php
├── RedisClientTest.php
├── SampleTest.php
├── SampleWithLabelsTest.php
└── TimeSeriesTest.php
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | # Controls when the action will run.
4 | on:
5 | # Triggers the workflow on push or pull request events but only for the master branch
6 | push:
7 | branches: [ master ]
8 | pull_request:
9 | branches: [ master ]
10 |
11 | # Allows you to run this workflow manually from the Actions tab
12 | workflow_dispatch:
13 |
14 | jobs:
15 | build:
16 | name: PHP ${{ matrix.php-version }} - RedisTimeSeries ${{ matrix.rts-version }}
17 | runs-on: ubuntu-latest
18 | strategy:
19 | matrix:
20 | php-version: ['7.2', '7.3', '7.4', '8.0']
21 | rts-version: ['1.4.10']
22 | services:
23 | redis:
24 | image: redislabs/redistimeseries:${{ matrix.rts-version }}
25 | ports:
26 | - 6379:6379
27 | options: >-
28 | --health-cmd="redis-cli ping"
29 | --health-interval=10s
30 | --health-timeout=5s
31 | --health-retries=5
32 | --health-start-period=20s
33 | steps:
34 | - name: Checkout
35 | uses: actions/checkout@v2
36 | - name: Setup PHP
37 | uses: shivammathur/setup-php@v2
38 | with:
39 | php-version: ${{ matrix.php-version }}
40 | extensions: redis
41 | coverage: xdebug
42 | - name: Get composer cache directory
43 | id: composer-cache
44 | run: echo "::set-output name=dir::$(composer config cache-files-dir)"
45 | - name: Cache composer dependencies
46 | uses: actions/cache@v2
47 | with:
48 | path: ${{ steps.composer-cache.outputs.dir }}
49 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
50 | restore-keys: ${{ runner.os }}-composer-
51 | - name: Install dependencies
52 | run: |
53 | composer install --no-progress --prefer-dist --optimize-autoloader
54 | - name: Run tests
55 | run: vendor/bin/phpunit --coverage-text
56 | env:
57 | REDIS_HOST: localhost
58 | REDIS_TIMESERIES_VERSION: ${{ matrix.rts-version }}
59 | - name: Run psalm
60 | run: vendor/bin/psalm
61 | - name: Export coverage
62 | uses: paambaati/codeclimate-action@v2.7.5
63 | if: ${{ matrix.php-version == '7.4' && matrix.rts-version == '1.4.10' }}
64 | env:
65 | CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}}
66 | with:
67 | coverageLocations: clover.xml:clover
68 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea/
2 | /vendor/
3 | /clover.xml
4 | /.phpunit.result.cache
5 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:7.4-cli
2 | ARG DEBIAN_FRONTEND=noninteractive
3 | WORKDIR /app
4 |
5 | ENV XDEBUG_MODE=coverage
6 |
7 | RUN apt-get -y upgrade && \
8 | apt-get - dist-upgrade && \
9 | apt-get update && \
10 | apt-get install -yqq zip git wget
11 |
12 | RUN pecl install redis && \
13 | pecl install xdebug && \
14 | docker-php-ext-enable redis xdebug
15 |
16 | RUN wget https://github.com/composer/composer/releases/download/2.0.12/composer.phar -q && \
17 | mv composer.phar /usr/bin/composer && \
18 | chmod +x /usr/bin/composer
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Alessandro Balasco
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # phpRedisTimeSeries
2 |
3 | Use [Redis Time Series](https://oss.redislabs.com/redistimeseries/) in PHP!
4 |
5 | [](https://codeclimate.com/github/palicao/phpRedisTimeSeries/maintainability)
6 | [](https://codeclimate.com/github/palicao/phpRedisTimeSeries/test_coverage)
7 | [](https://travis-ci.com/palicao/phpRedisTimeSeries)
8 | [](https://packagist.org/packages/palicao/php-redis-time-series)
9 | 
10 | 
11 |
12 | ## Getting up and running
13 |
14 | ### Install
15 | `composer require palicao/php-redis-time-series`
16 |
17 | ### Requirements
18 | The library is tested against:
19 | - PHP 7.2, 7.3, 7.4, 8.0
20 | - RedisTimeSeries 1.4.10 (but it should work with any 1.4 version)
21 |
22 | In order to use RedisTimeSeries 1.2 please use version 2.1.1 of this library.
23 |
24 | ### Construct
25 | ```
26 | $ts = new TimeSeries(
27 | new RedisClient(
28 | new Redis(),
29 | new RedisConnectionParams($host, $port)
30 | )
31 | );
32 | ```
33 |
34 | ## `TimeSeries` methods
35 |
36 | ### `create`
37 |
38 | `TimeSeries::create(string $key, ?int $retentionMs = null, array $labels = []): void`
39 |
40 | Creates a key, optionally setting a retention time (in milliseconds) and some labels.
41 |
42 | See https://oss.redislabs.com/redistimeseries/commands/#tscreate.
43 |
44 | ### `alter`
45 |
46 | `TimeSeries::alter(string $key, ?int $retentionMs = null, array $labels = []): void`
47 |
48 | Modifies an existing key's retention time and/or labels.
49 |
50 | See https://oss.redislabs.com/redistimeseries/commands/#tsalter.
51 |
52 | ### `add`
53 |
54 | `TimeSeries::add(Sample $sample, ?int $retentionMs = null, array $labels = []): Sample`
55 |
56 | Adds a sample.
57 |
58 | If the key was not explicitly `create`d, it's possible to set retention and labels.
59 |
60 | See https://oss.redislabs.com/redistimeseries/commands/#tsadd.
61 |
62 | Examples:
63 | * `$sample = $ts->add(new Sample('myKey', 32.10), 10, [new Label('myLabel', 'myValue')]);` Adds a sample to `myKey` at
64 | the current timestamp (time of the redis server) and returns it (complete with dateTime).
65 | * `$sample = $ts->add(new Sample('myKey', 32.10, new DateTimeImmutable()), 10, [new Label('myLabel', 'myValue')]);`
66 | Adds a sample to `myKey` at a given datetime and returns it.
67 |
68 | Please notice that RedisTimeSeries only allows to add samples in order (no sample older than the latest is allowed)
69 |
70 | ### `addMany`
71 |
72 | `TimeSeries::addMany(array $samples): array`
73 |
74 | Adds several samples.
75 |
76 | As usual, if no timestamp is provided, the redis server current time is used. Added samples are returned in an array.
77 |
78 | See https://oss.redislabs.com/redistimeseries/commands/#tsmadd.
79 |
80 | ### `incrementBy` and `decrementBy`
81 |
82 | `TimeSeries::incrementBy(Sample $sample, ?int $resetMs = null, ?int $retentionMs = null, array $labels = []): void`
83 |
84 | `TimeSeries::decrementBy(Sample $sample, ?int $resetMs = null, ?int $retentionMs = null, array $labels = []): void`
85 |
86 | Add a sample to a key, incrementing or decrementing the last value by an amount specified in `$sample`.
87 | The value can be optionally reset after `$resetMs` milliseconds.
88 |
89 | Similarly to `add`, if the command is used on a non-existing key, it's possible to set retention and labels.
90 |
91 | See https://oss.redislabs.com/redistimeseries/commands/#tsincrbytsdecrby.
92 |
93 | ### `createRule`
94 |
95 | `TimeSeries::createRule(string $sourceKey, string $destKey, AggregationRule $rule): void`
96 |
97 | Creates an aggregation rule which applies the given `$rule` to `$sourceKey` in order to populate `$destKey`.
98 |
99 | Notice that both key must already exist otherwise the command will fail.
100 |
101 | See https://oss.redislabs.com/redistimeseries/commands/#tscreaterule.
102 |
103 | Example:
104 |
105 | * `$ts->createRule('source', 'destination', new AggregationRule(AggregationRule::AVG, 10000)`, will populate the
106 | `destination` key by averaging the samples contained in `source` over 10 second buckets.
107 |
108 | ### `deleteRule`
109 |
110 | `TimeSeries::deleteRule(string $sourceKey, string $destKey): void`
111 |
112 | Deletes an existing aggregation rule.
113 |
114 | See https://oss.redislabs.com/redistimeseries/commands/#tsdeleterule.
115 |
116 | ### `range`
117 |
118 | `TimeSeries::range(string $key, ?DateTimeInterface $from = null, ?DateTimeInterface $to = null, ?int $count = null, ?AggregationRule $rule = null, bool $reverse = false): Sample[]`
119 |
120 | Retrieves samples from a key. It's possible to limit the query in a given time frame (passing `$from` and `$to`),
121 | to limit the retrieved amount of samples (passing `$count`), and also to pre-aggregate the results using a `$rule`.
122 |
123 | The flag `$reverse` will return the results in reverse time order.
124 |
125 | See https://oss.redislabs.com/redistimeseries/commands/#tsrange.
126 |
127 | ### `multiRange` and `multiRangeWithLabels`
128 |
129 | `TimeSeries::multiRange(Filter $filter, ?DateTimeInterface $from = null, ?DateTimeInterface $to = null, ?int $count = null, ?AggregationRule $rule = null, bool $reverse = false): Sample[]`
130 |
131 | `TimeSeries::multiRangeWithLabels(Filter $filter, ?DateTimeInterface $from = null, ?DateTimeInterface $to = null, ?int $count = null, ?AggregationRule $rule = null, bool $reverse = false): SampleWithLabels[]`
132 |
133 | Similar to `range`, but instead of querying by key, queries for a specific set of labels specified in `$filter`.
134 |
135 | `multiRangeWithLabels` will return an array of `SampleWithLabels` instead of simple `Sample`s.
136 |
137 | The flag `$reverse` will return the results in reverse time order.
138 |
139 | See https://oss.redislabs.com/redistimeseries/commands/#tsmrange.
140 |
141 | ### `getLastSample`
142 |
143 | `TimeSeries::getLastSample(string $key): Sample`
144 |
145 | Gets the last sample for a key.
146 |
147 | See https://oss.redislabs.com/redistimeseries/commands/#tsget.
148 |
149 | ### `getLastSamples`
150 |
151 | `TimeSeries::getLastSamples(Filter $filter): Sample[]`
152 |
153 | Gets the last sample for a set of keys matching a given `$filter`.
154 |
155 | See https://oss.redislabs.com/redistimeseries/commands/#tsmget.
156 |
157 | ### `info`
158 |
159 | `TimeSeries::info(string $key): Metadata`
160 |
161 | Gets useful metadata for a given key.
162 |
163 | See https://oss.redislabs.com/redistimeseries/commands/#tsinfo.
164 |
165 | ### `getKeysByFilter`
166 |
167 | `getKeysByFilter(Filter $filter): string[]`
168 |
169 | Retrieves the list of keys matching a given `$filter`.
170 |
171 | See https://oss.redislabs.com/redistimeseries/commands/#tsqueryindex.
172 |
173 | ## Advanced connection parameters
174 |
175 | You can manipulate your `RedisConnectionParams` using the following methods:
176 |
177 | * `RedisConnectionParams::setPersistentConnection(bool $persistentConnection)` enable/disable a persistent connection
178 | * `RedisConnectionParams::setTimeout(int $timeout)` connection timeout in seconds
179 | * `RedisConnectionParams::setRetryInterval(int $retryInterval)` retry interval in seconds
180 | * `RedisConnectionParams::setReadTimeout(float $readTimeout)` read timeout in seconds
181 |
182 | ## Building Filters
183 |
184 | A `Filter` is composed of multiple filtering operations. At least one equality operation must be provided (in the
185 | constructor). Adding filtering operations can be chained using the method `add`.
186 |
187 | Examples:
188 |
189 | * `$f = (new Filter('label1', 'value1'))->add('label2', Filter::OP_EXISTS);` filters items which have label1 = value1
190 | and have label2.
191 | * `$f = (new Filter('label1', 'value1'))->add('label2', Filter::OP_IN, ['a', 'b', 'c']);` filters items which have
192 | label1 = value1 and where label2 is one of 'a', 'b' or 'c'.
193 |
194 | ## Local testing
195 | For local testing you can use the provided `docker-compose.yml` file, which will create a PHP container (with the redis
196 | extension pre-installed), and a redis container (with Redis Time Series included).
197 |
198 | ## Contributors
199 | Thanks [Mrkisha](https://github.com/Mrkisha) for the precious contributions.
200 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "palicao/php-redis-time-series",
3 | "description": "Use Redis Time Series in PHP",
4 | "license": "MIT",
5 | "authors": [
6 | {
7 | "name": "Alessandro Balasco",
8 | "email": "alessandro.balasco@gmail.com"
9 | }
10 | ],
11 | "autoload": {
12 | "psr-4": {
13 | "Palicao\\PhpRedisTimeSeries\\": "src"
14 | }
15 | },
16 | "autoload-dev": {
17 | "psr-4": {
18 | "Palicao\\PhpRedisTimeSeries\\Tests\\": "tests"
19 | }
20 | },
21 | "require": {
22 | "php": ">=7.2.0",
23 | "ext-redis": "*"
24 | },
25 | "require-dev": {
26 | "phpunit/phpunit": "^8.3",
27 | "vimeo/psalm": "^4.7"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | php-rts-php:
5 | build:
6 | dockerfile: Dockerfile
7 | context: .
8 | entrypoint: sleep infinity
9 | volumes:
10 | - .:/app
11 | - ./docker/xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
12 | php-rts-redis:
13 | image: redislabs/redistimeseries:1.4.10
14 |
--------------------------------------------------------------------------------
/docker/xdebug.ini:
--------------------------------------------------------------------------------
1 | zend_extension=xdebug
2 |
3 | [xdebug]
4 | xdebug.mode=develop,debug
5 | xdebug.client_host=host.docker.internal
6 | xdebug.start_with_request=yes
7 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 | ./tests/
9 |
10 |
11 |
12 |
13 | ./src/
14 |
15 | ./src/Exception
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/AggregationRule.php:
--------------------------------------------------------------------------------
1 | type = $type;
57 | $this->timeBucketMs = $timeBucketMs;
58 | }
59 |
60 | /**
61 | * @codeCoverageIgnore
62 | */
63 | public function getType(): string
64 | {
65 | return $this->type;
66 | }
67 |
68 | /**
69 | * @codeCoverageIgnore
70 | */
71 | public function getTimeBucketMs(): int
72 | {
73 | return $this->timeBucketMs;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Client/RedisClient.php:
--------------------------------------------------------------------------------
1 | redis = $redis;
22 | $this->connectionParams = $connectionParams;
23 | }
24 |
25 | /**
26 | * @throws RedisClientException
27 | */
28 | private function connectIfNeeded(): void
29 | {
30 | if ($this->redis->isConnected()) {
31 | return;
32 | }
33 |
34 | $params = $this->connectionParams;
35 |
36 | if ($params->isPersistentConnection()) {
37 | /** @psalm-suppress TooManyArguments */
38 | $result = $this->redis->pconnect(
39 | $params->getHost(),
40 | $params->getPort(),
41 | $params->getTimeout(),
42 | gethostname(),
43 | $params->getRetryInterval(),
44 | $params->getReadTimeout()
45 | );
46 | } else {
47 | /** @psalm-suppress InvalidArgument */
48 | $result = $this->redis->connect(
49 | $params->getHost(),
50 | $params->getPort(),
51 | $params->getTimeout(),
52 | (PHP_VERSION_ID >= 70300) ? null : '',
53 | $params->getRetryInterval(),
54 | $params->getReadTimeout()
55 | );
56 | }
57 |
58 | if ($result === false) {
59 | throw new RedisClientException(sprintf(
60 | 'Unable to connect to redis server %s:%s: %s',
61 | $params->getHost(),
62 | $params->getPort(),
63 | $this->redis->getLastError() ?? 'unknown error'
64 | ));
65 | }
66 |
67 | $this->authenticate($params->getUsername(), $params->getPassword());
68 | }
69 |
70 | /**
71 | * @param string[] $params
72 | * @return mixed
73 | * @throws RedisException
74 | * @throws RedisClientException
75 | */
76 | public function executeCommand(array $params)
77 | {
78 | $this->connectIfNeeded();
79 | // UNDOCUMENTED FEATURE: option 8 is REDIS_OPT_REPLY_LITERAL
80 | $value = (PHP_VERSION_ID < 70300) ? '1' : 1;
81 | $this->redis->setOption(8, $value);
82 | return $this->redis->rawCommand(...$params);
83 | }
84 |
85 | /**
86 | * @param string|null $username
87 | * @param string|null $password
88 | * @throws RedisAuthenticationException
89 | */
90 | private function authenticate(?string $username, ?string $password): void
91 | {
92 | try {
93 | if ($password) {
94 | if ($username) {
95 | // Calling auth() with an array throws a TypeError in some cases
96 | /**
97 | * @noinspection PhpMethodParametersCountMismatchInspection
98 | * @var bool $result
99 | */
100 | $result = $this->redis->rawCommand('AUTH', $username, $password);
101 | } else {
102 | /** @psalm-suppress PossiblyNullArgument */
103 | $result = $this->redis->auth($password);
104 | }
105 | if ($result === false) {
106 | throw new RedisAuthenticationException(sprintf(
107 | 'Failure authenticating user %s', $username ?: 'default'
108 | ));
109 | }
110 | }
111 | } /** @noinspection PhpRedundantCatchClauseInspection */ catch (RedisException $e) {
112 | throw new RedisAuthenticationException(sprintf(
113 | 'Failure authenticating user %s: %s', $username ?: 'default', $e->getMessage()
114 | ));
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/Client/RedisClientInterface.php:
--------------------------------------------------------------------------------
1 | persistentConnection = false;
46 | $this->host = $host;
47 | $this->port = $port;
48 | $this->username = $username;
49 | $this->password = $password;
50 | $this->timeout = 0;
51 | $this->retryInterval = 0;
52 | $this->readTimeout = 0.0;
53 | }
54 |
55 | /**
56 | * Whether to use a persistent connection
57 | * @param bool $persistentConnection
58 | * @return RedisConnectionParams
59 | */
60 | public function setPersistentConnection(bool $persistentConnection): RedisConnectionParams
61 | {
62 | $this->persistentConnection = $persistentConnection;
63 | return $this;
64 | }
65 |
66 | /**
67 | * Connection timeout (in seconds)
68 | * @param int $timeout
69 | * @return RedisConnectionParams
70 | */
71 | public function setTimeout(int $timeout): RedisConnectionParams
72 | {
73 | $this->timeout = $timeout;
74 | return $this;
75 | }
76 |
77 | /**
78 | * Retry interval (in seconds)
79 | * @param int $retryInterval
80 | * @return RedisConnectionParams
81 | */
82 | public function setRetryInterval(int $retryInterval): RedisConnectionParams
83 | {
84 | $this->retryInterval = $retryInterval;
85 | return $this;
86 | }
87 |
88 | /**
89 | * Read timeout in seconds
90 | * @param float $readTimeout
91 | * @return RedisConnectionParams
92 | */
93 | public function setReadTimeout(float $readTimeout): RedisConnectionParams
94 | {
95 | $this->readTimeout = $readTimeout;
96 | return $this;
97 | }
98 |
99 | public function isPersistentConnection(): bool
100 | {
101 | return $this->persistentConnection;
102 | }
103 |
104 | public function getHost(): string
105 | {
106 | return $this->host;
107 | }
108 |
109 | public function getPort(): int
110 | {
111 | return $this->port;
112 | }
113 |
114 | public function getTimeout(): int
115 | {
116 | return $this->timeout;
117 | }
118 |
119 | public function getRetryInterval(): int
120 | {
121 | return $this->retryInterval;
122 | }
123 |
124 | public function getReadTimeout(): float
125 | {
126 | return $this->readTimeout;
127 | }
128 |
129 | public function getUsername(): ?string
130 | {
131 | return $this->username;
132 | }
133 |
134 | public function getPassword(): ?string
135 | {
136 | return $this->password;
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/DateTimeUtils.php:
--------------------------------------------------------------------------------
1 | format('Uu') / 1000);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Exception/InvalidAggregationException.php:
--------------------------------------------------------------------------------
1 |
29 | */
30 | private $filters = [];
31 |
32 | public function __construct(string $label, string $value)
33 | {
34 | $this->filters[] = [$label, self::OP_EQUALS, $value];
35 | }
36 |
37 | /**
38 | * @param string $label
39 | * @param int $operation
40 | * @param string|array|null $value
41 | * @return self
42 | */
43 | public function add(string $label, int $operation, $value = null): self
44 | {
45 | if (!in_array($operation, self::OPERATIONS, true)) {
46 | throw new InvalidFilterOperationException('Operation is not valid');
47 | }
48 |
49 | if (!is_string($value) && in_array($operation, [self::OP_EQUALS, self::OP_NOT_EQUALS], true)) {
50 | throw new InvalidFilterOperationException('The provided operation requires the value to be string');
51 | }
52 |
53 | if ($value !== null && in_array($operation, [self::OP_EXISTS, self::OP_NOT_EXISTS], true)) {
54 | throw new InvalidFilterOperationException('The provided operation requires the value to be null');
55 | }
56 |
57 | if (!is_array($value) && in_array($operation, [self::OP_IN, self::OP_NOT_IN], true)) {
58 | throw new InvalidFilterOperationException('The provided operation requires the value to be an array');
59 | }
60 |
61 | $this->filters[] = [$label, $operation, $value];
62 |
63 | return $this;
64 | }
65 |
66 | /**
67 | * @return string[]
68 | */
69 | public function toRedisParams(): array
70 | {
71 | $params = [];
72 | foreach ($this->filters as $filter) {
73 | switch ($filter[1]) {
74 | case self::OP_EQUALS:
75 | assert(is_string($filter[2]));
76 | $params[] = $filter[0] . '=' . $filter[2];
77 | break;
78 | case self::OP_NOT_EQUALS:
79 | assert(is_string($filter[2]));
80 | $params[] = $filter[0] . '!=' . $filter[2];
81 | break;
82 | case self::OP_EXISTS:
83 | $params[] = $filter[0] . '=';
84 | break;
85 | case self::OP_NOT_EXISTS:
86 | $params[] = $filter[0] . '!=';
87 | break;
88 | case self::OP_IN:
89 | assert(is_array($filter[2]));
90 | $params[] = $filter[0] . '=(' . implode(',', $filter[2]) . ')';
91 | break;
92 | case self::OP_NOT_IN:
93 | assert(is_array($filter[2]));
94 | $params[] = $filter[0] . '!=(' . implode(',', $filter[2]) . ')';
95 | break;
96 | }
97 | }
98 | return $params;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/Label.php:
--------------------------------------------------------------------------------
1 | key = $key;
22 | $this->value = $value;
23 | }
24 |
25 | /**
26 | * @codeCoverageIgnore
27 | */
28 | public function getKey(): string
29 | {
30 | return $this->key;
31 | }
32 |
33 | /**
34 | * @codeCoverageIgnore
35 | */
36 | public function getValue(): string
37 | {
38 | return $this->value;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Metadata.php:
--------------------------------------------------------------------------------
1 | lastTimestamp = $lastTimestamp;
51 | $this->retentionTime = $retentionTime;
52 | $this->chunkCount = $chunkCount;
53 | $this->maxSamplesPerChunk = $maxSamplesPerChunk;
54 | $this->labels = $labels;
55 | $this->sourceKey = $sourceKey;
56 | $this->rules = $rules;
57 | }
58 |
59 | /**
60 | * @param int $lastTimestamp
61 | * @param int $retentionTime
62 | * @param int $chunkCount
63 | * @param int $maxSamplesPerChunk
64 | * @param Label[] $labels
65 | * @param string|null $sourceKey
66 | * @param AggregationRule[] $rules
67 | * @return static
68 | */
69 | public static function fromRedis(
70 | int $lastTimestamp,
71 | int $retentionTime = 0,
72 | int $chunkCount = 0,
73 | int $maxSamplesPerChunk = 0,
74 | array $labels = [],
75 | ?string $sourceKey = null,
76 | array $rules = []
77 | ): self
78 | {
79 | $dateTime = DateTimeUtils::dateTimeFromTimestampWithMs($lastTimestamp);
80 | return new self($dateTime, $retentionTime, $chunkCount, $maxSamplesPerChunk, $labels, $sourceKey, $rules);
81 | }
82 |
83 | /**
84 | * @return DateTimeInterface
85 | */
86 | public function getLastTimestamp(): DateTimeInterface
87 | {
88 | return $this->lastTimestamp;
89 | }
90 |
91 | /**
92 | * @return int
93 | */
94 | public function getRetentionTime(): int
95 | {
96 | return $this->retentionTime;
97 | }
98 |
99 | /**
100 | * @return int
101 | */
102 | public function getChunkCount(): int
103 | {
104 | return $this->chunkCount;
105 | }
106 |
107 | /**
108 | * @return int
109 | */
110 | public function getMaxSamplesPerChunk(): int
111 | {
112 | return $this->maxSamplesPerChunk;
113 | }
114 |
115 | /**
116 | * @return Label[]
117 | */
118 | public function getLabels(): array
119 | {
120 | return $this->labels;
121 | }
122 |
123 | /**
124 | * @return string
125 | */
126 | public function getSourceKey(): ?string
127 | {
128 | return $this->sourceKey;
129 | }
130 |
131 | /**
132 | * @return AggregationRule[]
133 | */
134 | public function getRules(): array
135 | {
136 | return $this->rules;
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/Sample.php:
--------------------------------------------------------------------------------
1 | key = $key;
23 | $this->value = $value;
24 | $this->dateTime = $dateTime;
25 | }
26 |
27 | public static function createFromTimestamp(string $key, float $value, int $timestamp): Sample
28 | {
29 | return new self($key, $value, DateTimeUtils::dateTimeFromTimestampWithMs($timestamp));
30 | }
31 |
32 | public function getKey(): string
33 | {
34 | return $this->key;
35 | }
36 |
37 | public function getValue(): float
38 | {
39 | return $this->value;
40 | }
41 |
42 | public function getDateTime(): ?DateTimeInterface
43 | {
44 | return $this->dateTime;
45 | }
46 |
47 | /**
48 | * @return string
49 | * @psalm-external-mutation-free
50 | */
51 | public function getTimestampWithMs(): string
52 | {
53 | if ($this->dateTime === null) {
54 | return '*';
55 | }
56 | return (string)DateTimeUtils::timestampWithMsFromDateTime($this->dateTime);
57 | }
58 |
59 | /**
60 | * @return string[]
61 | */
62 | public function toRedisParams(): array
63 | {
64 | return [$this->getKey(), $this->getTimestampWithMs(), (string) $this->getValue()];
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/SampleWithLabels.php:
--------------------------------------------------------------------------------
1 | labels = $labels;
25 | }
26 |
27 | /**
28 | * @param string $key
29 | * @param float $value
30 | * @param int $timestamp
31 | * @param Label[] $labels
32 | * @return SampleWithLabels
33 | */
34 | public static function createFromTimestampAndLabels(
35 | string $key,
36 | float $value,
37 | int $timestamp,
38 | array $labels
39 | ): SampleWithLabels
40 | {
41 | return new self($key, $value, DateTimeUtils::dateTimeFromTimestampWithMs($timestamp), $labels);
42 | }
43 |
44 | /**
45 | * @return Label[]
46 | */
47 | public function getLabels(): array
48 | {
49 | return $this->labels;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/TimeSeries.php:
--------------------------------------------------------------------------------
1 | redis = $redis;
40 | }
41 |
42 | /**
43 | * Creates a key
44 | * @see https://oss.redislabs.com/redistimeseries/commands/#tscreate
45 | * @param string $key
46 | * @param int|null $retentionMs
47 | * @param Label[] $labels
48 | * @param bool $uncompressed
49 | * @param int|null $chunkSize
50 | * @param string|null $duplicatePolicy
51 | * @return void
52 | * @throws RedisException
53 | */
54 | public function create(
55 | string $key,
56 | ?int $retentionMs = null,
57 | array $labels = [],
58 | bool $uncompressed = false,
59 | ?int $chunkSize = null,
60 | ?string $duplicatePolicy = null
61 | ): void
62 | {
63 | $params = [];
64 |
65 | if ($uncompressed === true) {
66 | $params[] = 'UNCOMPRESSED';
67 | }
68 |
69 | if ($chunkSize !== null) {
70 | $params[] = 'CHUNK_SIZE';
71 | $params[] = (string) $chunkSize;
72 | }
73 |
74 | if ($duplicatePolicy !== null) {
75 | if (!in_array($duplicatePolicy, self::DUPLICATE_POLICIES)) {
76 | throw new InvalidDuplicatePolicyException(sprintf("Duplicate policy %s is invalid", $duplicatePolicy));
77 | }
78 | $params[] = 'DUPLICATE_POLICY';
79 | $params[] = $duplicatePolicy;
80 | }
81 |
82 | $this->redis->executeCommand(array_merge(
83 | ['TS.CREATE', $key],
84 | $this->getRetentionParams($retentionMs),
85 | $params,
86 | $this->getLabelsParams(...$labels)
87 | ));
88 | }
89 |
90 | /**
91 | * Modifies an existing key
92 | * @see https://oss.redislabs.com/redistimeseries/commands/#tsalter
93 | * @param string $key
94 | * @param int|null $retentionMs
95 | * @param Label[] $labels
96 | * @return void
97 | * @throws RedisClientException
98 | * @throws RedisException
99 | */
100 | public function alter(string $key, ?int $retentionMs = null, array $labels = []): void
101 | {
102 | $this->redis->executeCommand(array_merge(
103 | ['TS.ALTER', $key],
104 | $this->getRetentionParams($retentionMs),
105 | $this->getLabelsParams(...$labels)
106 | ));
107 | }
108 |
109 | /**
110 | * Adds a sample
111 | * @see https://oss.redislabs.com/redistimeseries/commands/#tsadd
112 | * @param Sample $sample
113 | * @param int|null $retentionMs
114 | * @param Label[] $labels
115 | * @param bool $uncompressed
116 | * @param int|null $chunkSize
117 | * @param string|null $duplicatePolicy
118 | * @return Sample
119 | * @throws RedisException
120 | */
121 | public function add(
122 | Sample $sample,
123 | ?int $retentionMs = null,
124 | array $labels = [],
125 | bool $uncompressed = false,
126 | ?int $chunkSize = null,
127 | ?string $duplicatePolicy = null
128 | ): Sample
129 | {
130 | $params = [];
131 |
132 | if ($uncompressed === true) {
133 | $params[] = 'UNCOMPRESSED';
134 | }
135 |
136 | if ($chunkSize !== null) {
137 | $params[] = 'CHUNK_SIZE';
138 | $params[] = (string) $chunkSize;
139 | }
140 |
141 | if ($duplicatePolicy !== null) {
142 | if (!in_array($duplicatePolicy, self::DUPLICATE_POLICIES)) {
143 | throw new InvalidDuplicatePolicyException(sprintf("Duplicate policy %s is invalid", $duplicatePolicy));
144 | }
145 | $params[] = 'ON_DUPLICATE';
146 | $params[] = $duplicatePolicy;
147 | }
148 |
149 | $timestamp = (int)$this->redis->executeCommand(array_merge(
150 | ['TS.ADD'],
151 | $sample->toRedisParams(),
152 | $params,
153 | $this->getRetentionParams($retentionMs),
154 | $this->getLabelsParams(...$labels)
155 | ));
156 | return Sample::createFromTimestamp($sample->getKey(), $sample->getValue(), $timestamp);
157 | }
158 |
159 | /**
160 | * Adds many samples
161 | * @see https://oss.redislabs.com/redistimeseries/commands/#tsmadd
162 | * @param Sample[] $samples
163 | * @return Sample[]
164 | * @throws RedisClientException
165 | * @throws RedisException
166 | */
167 | public function addMany(array $samples): array
168 | {
169 | if (empty($samples)) {
170 | return [];
171 | }
172 | $params = ['TS.MADD'];
173 | foreach ($samples as $sample) {
174 | $sampleParams = $sample->toRedisParams();
175 | foreach ($sampleParams as $sampleParam) {
176 | $params[] = $sampleParam;
177 | }
178 | }
179 | /** @var int[] $timestamps */
180 | $timestamps = $this->redis->executeCommand($params);
181 | $count = count($timestamps);
182 | $results = [];
183 | for ($i = 0; $i < $count; $i++) {
184 | $results[] = Sample::createFromTimestamp(
185 | $samples[$i]->getKey(),
186 | $samples[$i]->getValue(),
187 | $timestamps[$i]
188 | );
189 | }
190 | return $results;
191 | }
192 |
193 | /**
194 | * Increments a sample by the amount given in the passed sample
195 | * @see https://oss.redislabs.com/redistimeseries/commands/#tsincrbytsdecrby
196 | * @param Sample $sample
197 | * @param int|null $resetMs
198 | * @param int|null $retentionMs
199 | * @param Label[] $labels
200 | * @throws RedisClientException
201 | * @throws RedisException
202 | */
203 | public function incrementBy(Sample $sample, ?int $resetMs = null, ?int $retentionMs = null, array $labels = []): void
204 | {
205 | $this->incrementOrDecrementBy('TS.INCRBY', $sample, $resetMs, $retentionMs, $labels);
206 | }
207 |
208 | /**
209 | * Decrements a sample by the amount given in the passed sample
210 | * @see https://oss.redislabs.com/redistimeseries/commands/#tsincrbytsdecrby
211 | * @param Sample $sample
212 | * @param int|null $resetMs
213 | * @param int|null $retentionMs
214 | * @param Label[] $labels
215 | * @throws RedisClientException
216 | * @throws RedisException
217 | */
218 | public function decrementBy(Sample $sample, ?int $resetMs = null, ?int $retentionMs = null, array $labels = []): void
219 | {
220 | $this->incrementOrDecrementBy('TS.DECRBY', $sample, $resetMs, $retentionMs, $labels);
221 | }
222 |
223 | /**
224 | * @param string $op
225 | * @param Sample $sample
226 | * @param int|null $resetMs
227 | * @param int|null $retentionMs
228 | * @param Label[] $labels
229 | * @return void
230 | * @throws RedisClientException
231 | * @throws RedisException
232 | */
233 | private function incrementOrDecrementBy(
234 | string $op,
235 | Sample $sample,
236 | ?int $resetMs = null,
237 | ?int $retentionMs = null,
238 | array $labels = []
239 | ): void
240 | {
241 | $params = [$op, $sample->getKey(), (string)$sample->getValue()];
242 | if ($resetMs !== null) {
243 | $params[] = 'RESET';
244 | $params[] = (string)$resetMs;
245 | }
246 | if ($sample->getDateTime() !== null) {
247 | $params[] = 'TIMESTAMP';
248 | $params[] = $sample->getTimestampWithMs();
249 | }
250 | $params = array_merge(
251 | $params,
252 | $this->getRetentionParams($retentionMs),
253 | $this->getLabelsParams(...$labels)
254 | );
255 | $this->redis->executeCommand($params);
256 | }
257 |
258 | /**
259 | * Creates an aggregation rules for a key
260 | * @see https://oss.redislabs.com/redistimeseries/commands/#tscreaterule
261 | * @param string $sourceKey
262 | * @param string $destKey
263 | * @param AggregationRule $rule
264 | * @return void
265 | * @throws RedisClientException
266 | * @throws RedisException
267 | */
268 | public function createRule(string $sourceKey, string $destKey, AggregationRule $rule): void
269 | {
270 | $this->redis->executeCommand(array_merge(
271 | ['TS.CREATERULE', $sourceKey, $destKey],
272 | $this->getAggregationParams($rule)
273 | ));
274 | }
275 |
276 | /**
277 | * Deletes an existing aggregation rule
278 | * @see https://oss.redislabs.com/redistimeseries/commands/#tsdeleterule
279 | * @param string $sourceKey
280 | * @param string $destKey
281 | * @return void
282 | * @throws RedisClientException
283 | * @throws RedisException
284 | */
285 | public function deleteRule(string $sourceKey, string $destKey): void
286 | {
287 | $this->redis->executeCommand(['TS.DELETERULE', $sourceKey, $destKey]);
288 | }
289 |
290 | /**
291 | * Gets samples for a key, optionally aggregating them
292 | * @see https://oss.redislabs.com/redistimeseries/commands/#tsrange
293 | * @param string $key
294 | * @param DateTimeInterface|null $from
295 | * @param DateTimeInterface|null $to
296 | * @param int|null $count
297 | * @param AggregationRule|null $rule
298 | * @param bool $reverse
299 | * @return Sample[]
300 | * @throws RedisClientException
301 | * @throws RedisException
302 | */
303 | public function range(
304 | string $key,
305 | ?DateTimeInterface $from = null,
306 | ?DateTimeInterface $to = null,
307 | ?int $count = null,
308 | ?AggregationRule $rule = null,
309 | bool $reverse = false
310 | ): array
311 | {
312 | $fromTs = $from ? (string)DateTimeUtils::timestampWithMsFromDateTime($from) : '-';
313 | $toTs = $to ? (string)DateTimeUtils::timestampWithMsFromDateTime($to) : '+';
314 |
315 | $command = $reverse ? 'TS.REVRANGE' : 'TS.RANGE';
316 | $params = [$command, $key, $fromTs, $toTs];
317 | if ($count !== null) {
318 | $params[] = 'COUNT';
319 | $params[] = (string)$count;
320 | }
321 |
322 | $rawResults = $this->redis->executeCommand(array_merge($params, $this->getAggregationParams($rule)));
323 |
324 | $samples = [];
325 | foreach ($rawResults as $rawResult) {
326 | $samples[] = Sample::createFromTimestamp($key, (float)$rawResult[1], (int)$rawResult[0]);
327 | }
328 | return $samples;
329 | }
330 |
331 | /**
332 | * Gets samples from multiple keys, searching by a given filter.
333 | * @param Filter $filter
334 | * @param DateTimeInterface|null $from
335 | * @param DateTimeInterface|null $to
336 | * @param int|null $count
337 | * @param AggregationRule|null $rule
338 | * @param bool $reverse
339 | * @return Sample[]
340 | * @throws RedisClientException
341 | * @throws RedisException
342 | */
343 | public function multiRange(
344 | Filter $filter,
345 | ?DateTimeInterface $from = null,
346 | ?DateTimeInterface $to = null,
347 | ?int $count = null,
348 | ?AggregationRule $rule = null,
349 | bool $reverse = false
350 | ): array
351 | {
352 | $results = $this->multiRangeRaw($filter, $from, $to, $count, $rule, $reverse);
353 |
354 | $samples = [];
355 | foreach ($results as $groupByKey) {
356 | $key = $groupByKey[0];
357 | foreach ($groupByKey[2] as $result) {
358 | $samples[] = Sample::createFromTimestamp($key, (float)$result[1], (int)$result[0]);
359 | }
360 | }
361 | return $samples;
362 | }
363 |
364 | /**
365 | * @param Filter $filter
366 | * @param DateTimeInterface|null $from
367 | * @param DateTimeInterface|null $to
368 | * @param int|null $count
369 | * @param AggregationRule|null $rule
370 | * @param bool $reverse
371 | * @return SampleWithLabels[]
372 | * @throws RedisClientException
373 | * @throws RedisException
374 | */
375 | public function multiRangeWithLabels(
376 | Filter $filter,
377 | ?DateTimeInterface $from = null,
378 | ?DateTimeInterface $to = null,
379 | ?int $count = null,
380 | ?AggregationRule $rule = null,
381 | bool $reverse = false
382 | ): array
383 | {
384 | $results = $this->multiRangeRaw($filter, $from, $to, $count, $rule, $reverse, true);
385 |
386 | $samples = [];
387 | foreach ($results as $groupByKey) {
388 | $key = $groupByKey[0];
389 | $labels = [];
390 | foreach ($groupByKey[1] as $label) {
391 | $labels[] = new Label($label[0], $label[1]);
392 | }
393 | foreach ($groupByKey[2] as $result) {
394 | $samples[] = SampleWithLabels::createFromTimestampAndLabels(
395 | $key,
396 | (float)$result[1],
397 | $result[0],
398 | $labels
399 | );
400 | }
401 | }
402 | return $samples;
403 | }
404 |
405 | /**
406 | * @param Filter $filter
407 | * @param DateTimeInterface|null $from
408 | * @param DateTimeInterface|null $to
409 | * @param int|null $count
410 | * @param AggregationRule|null $rule
411 | * @param bool $reverse
412 | * @param bool $withLabels
413 | * @return array
414 | * @throws RedisException
415 | */
416 | private function multiRangeRaw(
417 | Filter $filter,
418 | ?DateTimeInterface $from = null,
419 | ?DateTimeInterface $to = null,
420 | ?int $count = null,
421 | ?AggregationRule $rule = null,
422 | bool $reverse = false,
423 | bool $withLabels = false
424 | ): array
425 | {
426 | $fromTs = $from ? (string)DateTimeUtils::timestampWithMsFromDateTime($from) : '-';
427 | $toTs = $to ? (string)DateTimeUtils::timestampWithMsFromDateTime($to) : '+';
428 |
429 | $command = $reverse ? 'TS.MREVRANGE' : 'TS.MRANGE';
430 | $params = [$command, $fromTs, $toTs];
431 |
432 | if ($count !== null) {
433 | $params[] = 'COUNT';
434 | $params[] = (string)$count;
435 | }
436 |
437 | $params = array_merge($params, $this->getAggregationParams($rule));
438 |
439 | if ($withLabels) {
440 | $params[] = 'WITHLABELS';
441 | }
442 |
443 | $params = array_merge($params, ['FILTER'], $filter->toRedisParams());
444 |
445 | return $this->redis->executeCommand($params);
446 | }
447 |
448 | /**
449 | * Gets the last sample for a key
450 | * @param string $key
451 | * @return Sample
452 | * @throws RedisClientException
453 | * @throws RedisException
454 | */
455 | public function getLastSample(string $key): Sample
456 | {
457 | $result = $this->redis->executeCommand(['TS.GET', $key]);
458 | return Sample::createFromTimestamp($key, (float)$result[1], (int)$result[0]);
459 | }
460 |
461 | /**
462 | * Gets the last samples for multiple keys using a filter
463 | * @param Filter $filter
464 | * @return array
465 | * @throws RedisClientException
466 | * @throws RedisException
467 | */
468 | public function getLastSamples(Filter $filter): array
469 | {
470 | $results = $this->redis->executeCommand(
471 | array_merge(['TS.MGET', 'FILTER'], $filter->toRedisParams())
472 | );
473 | $samples = [];
474 | foreach ($results as $result) {
475 | // most recent versions of TS.MGET return results in a nested array
476 | if (count($result) === 3) {
477 | $samples[] = Sample::createFromTimestamp($result[0], (float)$result[2][1], (int)$result[2][0]);
478 | } else {
479 | $samples[] = Sample::createFromTimestamp($result[0], (float)$result[3], (int)$result[2]);
480 | }
481 | }
482 | return $samples;
483 | }
484 |
485 | /**
486 | * Gets metadata regarding a key
487 | * @param string $key
488 | * @return Metadata
489 | * @throws RedisException
490 | */
491 | public function info(string $key): Metadata
492 | {
493 | $result = $this->redis->executeCommand(['TS.INFO', $key]);
494 |
495 | $labels = [];
496 | foreach ($result[9] as $strLabel) {
497 | $labels[] = new Label($strLabel[0], $strLabel[1]);
498 | }
499 |
500 | $sourceKey = $result[11] === false ? null : $result[11];
501 |
502 | $rules = [];
503 | foreach ($result[13] as $rule) {
504 | $rules[$rule[0]] = new AggregationRule($rule[2], $rule[1]);
505 | }
506 |
507 | return Metadata::fromRedis($result[1], $result[3], $result[5], $result[7], $labels, $sourceKey, $rules);
508 | }
509 |
510 | /**
511 | * Lists the keys matching a filter
512 | * @param Filter $filter
513 | * @return string[]
514 | * @throws RedisException
515 | */
516 | public function getKeysByFilter(Filter $filter): array
517 | {
518 | return $this->redis->executeCommand(
519 | array_merge(['TS.QUERYINDEX'], $filter->toRedisParams())
520 | );
521 | }
522 |
523 | /**
524 | * @param int|null $retentionMs
525 | * @return string[]
526 | */
527 | private function getRetentionParams(?int $retentionMs = null): array
528 | {
529 | if ($retentionMs === null) {
530 | return [];
531 | }
532 | return ['RETENTION', (string)$retentionMs];
533 | }
534 |
535 | /**
536 | * @param Label ...$labels
537 | * @return string[]
538 | */
539 | private function getLabelsParams(Label ...$labels): array
540 | {
541 | $params = [];
542 | foreach ($labels as $label) {
543 | $params[] = $label->getKey();
544 | $params[] = $label->getValue();
545 | }
546 |
547 | if (empty($params)) {
548 | return [];
549 | }
550 |
551 | array_unshift($params, 'LABELS');
552 | return $params;
553 | }
554 |
555 | /**
556 | * @param AggregationRule|null $rule
557 | * @return string[]
558 | */
559 | private function getAggregationParams(?AggregationRule $rule = null): array
560 | {
561 | if ($rule === null) {
562 | return [];
563 | }
564 | return ['AGGREGATION', $rule->getType(), (string)$rule->getTimeBucketMs()];
565 | }
566 | }
567 |
--------------------------------------------------------------------------------
/tests/Integration/IntegrationTest.php:
--------------------------------------------------------------------------------
1 | redisClient = new RedisClient(new Redis(), $connectionParams);
34 | $this->redisClient->executeCommand(['FLUSHDB']);
35 | $this->sut = new TimeSeries($this->redisClient);
36 | }
37 |
38 | protected function tearDown(): void
39 | {
40 | $this->redisClient->executeCommand(['FLUSHDB']);
41 | }
42 |
43 | public function testAddAndRetrieveAsRange(): void
44 | {
45 | $from = new DateTimeImmutable('2019-11-06 20:34:17.000');
46 | $to = new DateTimeImmutable('2019-11-06 20:34:17.100');
47 |
48 | $this->sut->create(
49 | 'temperature:3:11',
50 | 6000,
51 | [new Label('sensor_id', '2'), new Label('area_id', '32')]
52 | );
53 | $this->sut->add(new Sample('temperature:3:11', 30, $from));
54 | $this->sut->add(new Sample('temperature:3:11', 42, $to));
55 |
56 | $range = $this->sut->range(
57 | 'temperature:3:11',
58 | $from,
59 | $to,
60 | null,
61 | new AggregationRule(AggregationRule::AGG_AVG, 10)
62 | );
63 |
64 | $expectedRange = [
65 | new Sample('temperature:3:11', 30, new DateTimeImmutable('2019-11-06 20:34:17.000')),
66 | new Sample('temperature:3:11', 42, new DateTimeImmutable('2019-11-06 20:34:17.100'))
67 | ];
68 |
69 | self::assertEquals($expectedRange, $range);
70 | }
71 |
72 | public function testAddAndRetrieveWithDuplicatePolicySum(): void
73 | {
74 | $dt = new DateTimeImmutable('2019-11-06 20:34:17.000');
75 | $this->sut->create(
76 | 'temperature:3:11',
77 | 6000,
78 | [new Label('sensor_id', '2'), new Label('area_id', '32')],
79 | false,
80 | null,
81 | TimeSeries::DUPLICATE_POLICY_SUM
82 | );
83 |
84 | $this->sut->add(new Sample('temperature:3:11', 10.0, $dt));
85 | $this->sut->add(new Sample('temperature:3:11', 20.0, $dt));
86 |
87 | $result = $this->sut->range('temperature:3:11', $dt, $dt);
88 |
89 | self::assertEquals([new Sample('temperature:3:11', 30.0, $dt)], $result);
90 | }
91 |
92 | public function testAddAndRetrieveAsMultirangeWithLabelsReverse(): void
93 | {
94 | $this->sut->create(
95 | 'temperature:3:11',
96 | 60000,
97 | [new Label('sensor_id', '3'), new Label('area_id', '11')]
98 | );
99 | $this->sut->add(
100 | new Sample('temperature:3:11', 30, new DateTimeImmutable('2019-11-06 20:34:10.400'))
101 | );
102 | $this->sut->add(
103 | new Sample('temperature:3:11', 42, new DateTimeImmutable('2019-11-06 20:34:11.400'))
104 | );
105 |
106 | $this->sut->create(
107 | 'temperature:3:12',
108 | 60000,
109 | [new Label('sensor_id', '3'), new Label('area_id', '12')]
110 | );
111 | $this->sut->add(
112 | new Sample('temperature:3:12', 34, new DateTimeImmutable('2019-11-06 20:34:10.000'))
113 | );
114 | $this->sut->add(
115 | new Sample('temperature:3:12', 48, new DateTimeImmutable('2019-11-06 20:34:11.000'))
116 | );
117 |
118 | $range = $this->sut->multiRangeWithLabels(
119 | new Filter('sensor_id', '3'),
120 | null,
121 | null,
122 | null,
123 | new AggregationRule(AggregationRule::AGG_AVG, 60000), // 1-minute aggregation
124 | true
125 | );
126 |
127 | $expectedRange = [
128 | new SampleWithLabels(
129 | 'temperature:3:11',
130 | 36, // average between 30 and 42
131 | new DateTimeImmutable('2019-11-06 20:34:00.000'),
132 | [new Label('sensor_id', '3'), new Label('area_id', '11')]
133 | ),
134 | new SampleWithLabels(
135 | 'temperature:3:12',
136 | 41, //average beween 34 and 48
137 | new DateTimeImmutable('2019-11-06 20:34:00.000'),
138 | [new Label('sensor_id', '3'), new Label('area_id', '12')]
139 | ),
140 | ];
141 |
142 | self::assertEquals($expectedRange, $range);
143 | }
144 |
145 | public function testAddAndRetrieveAsMultiRangeWithMultipleFilters(): void
146 | {
147 | $from = new DateTimeImmutable('2019-11-06 20:34:17.000');
148 | $to = new DateTimeImmutable('2019-11-06 20:34:17.100');
149 |
150 | $this->sut->create(
151 | 'temperature:3:11',
152 | 6000,
153 | [new Label('sensor_id', '2'), new Label('area_id', '32')]
154 | );
155 | $this->sut->add(new Sample('temperature:3:11', 30, $from));
156 | $this->sut->add(new Sample('temperature:3:11', 42, $to));
157 |
158 | $filter = new Filter('sensor_id', '2');
159 | $filter->add('area_id', Filter::OP_EQUALS, '32');
160 |
161 | $range = $this->sut->multiRange($filter);
162 |
163 | $expectedRange = [
164 | new Sample('temperature:3:11', 30, new DateTimeImmutable('2019-11-06 20:34:17.000')),
165 | new Sample('temperature:3:11', 42, new DateTimeImmutable('2019-11-06 20:34:17.100'))
166 | ];
167 |
168 | self::assertEquals($expectedRange, $range);
169 | }
170 |
171 | public function testAddAndRetrieveAsLastSamplesWithMultipleFilters(): void
172 | {
173 | $from = new DateTimeImmutable('2019-11-06 20:34:17.000');
174 | $to = new DateTimeImmutable('2019-11-06 20:34:18.000');
175 |
176 | $this->sut->create(
177 | 'temperature:3:11',
178 | 6000,
179 | [new Label('sensor_id', '2'), new Label('area_id', '32')]
180 | );
181 | $this->sut->add(new Sample('temperature:3:11', 30, $from));
182 | $this->sut->add(new Sample('temperature:3:11', 42, $to));
183 |
184 | $this->sut->create(
185 | 'temperature:3:12',
186 | 6000,
187 | [new Label('sensor_id', '2'), new Label('area_id', '32')]
188 | );
189 | $this->sut->add(new Sample('temperature:3:12', 30, $from));
190 | $this->sut->add(new Sample('temperature:3:12', 42, $to));
191 |
192 | $filter = new Filter('sensor_id', '2');
193 | $filter->add('area_id', Filter::OP_EQUALS, '32');
194 |
195 | $range = $this->sut->getLastSamples($filter);
196 |
197 | $expectedResult = [
198 | new Sample('temperature:3:11', 42, new DateTimeImmutable('2019-11-06 20:34:18.000')),
199 | new Sample('temperature:3:12', 42, new DateTimeImmutable('2019-11-06 20:34:18.000'))
200 | ];
201 |
202 | self::assertEquals($expectedResult, $range);
203 | }
204 |
205 | public function testAddAndRetrieveKeysWithMultipleFilters(): void
206 | {
207 | $from = new DateTimeImmutable('2019-11-06 20:34:17.000');
208 | $to = new DateTimeImmutable('2019-11-06 20:34:17.100');
209 |
210 | $this->sut->create(
211 | 'temperature:3:11',
212 | 6000,
213 | [new Label('sensor_id', '2'), new Label('area_id', '32')]
214 | );
215 | $this->sut->add(new Sample('temperature:3:11', 30, $from));
216 | $this->sut->add(new Sample('temperature:3:11', 42, $to));
217 |
218 | $this->sut->create(
219 | 'temperature:3:12',
220 | 6000,
221 | [new Label('sensor_id', '2'), new Label('area_id', '32')]
222 | );
223 | $this->sut->add(new Sample('temperature:3:12', 30, $from));
224 | $this->sut->add(new Sample('temperature:3:12', 42, $to));
225 |
226 | $filter = new Filter('sensor_id', '2');
227 | $filter->add('area_id', Filter::OP_EQUALS, '32');
228 |
229 | $range = $this->sut->getKeysByFilter($filter);
230 |
231 | $expectedResult = ['temperature:3:11', 'temperature:3:12'];
232 |
233 | self::assertEquals($expectedResult, $range);
234 | }
235 |
236 | public function testAddAndRetrieveWithDateTimeObjectAsMultiRangeWithMultipleFilters(): void
237 | {
238 | $currentDate = new DateTime();
239 | $from = (clone $currentDate)->sub(new DateInterval('P1D'));
240 | $to = $currentDate;
241 |
242 | $this->sut->create(
243 | 'temperature:3:11',
244 | 6000,
245 | [new Label('sensor_id', '2'), new Label('area_id', '32')]
246 | );
247 | $this->sut->add(new Sample('temperature:3:11', 30, $from));
248 | $this->sut->add(new Sample('temperature:3:11', 42, $to));
249 |
250 | $filter = new Filter('sensor_id', '2');
251 | $filter->add('area_id', Filter::OP_EQUALS, '32');
252 |
253 | $range = $this->sut->multiRange($filter);
254 |
255 | $expectedRange = [
256 | Sample::createFromTimestamp('temperature:3:11', (float)42, DateTimeUtils::timestampWithMsFromDateTime(new DateTimeImmutable($to->format('Y-m-d H:i:s.u'))))
257 | ];
258 |
259 | self::assertEquals($expectedRange, $range);
260 | }
261 |
262 | public function testAddAndRetrieveWithDateTimeObjectAsRange(): void
263 | {
264 | $from = new DateTimeImmutable('2019-11-06 20:34:17.103000');
265 | $to = new DateTimeImmutable('2019-11-06 20:34:17.107000');
266 |
267 | $this->sut->create(
268 | 'temperature:3:11',
269 | null,
270 | [new Label('sensor_id', '2'), new Label('area_id', '32')]
271 | );
272 |
273 | $this->sut->add(new Sample('temperature:3:11', 30, $from));
274 | $this->sut->add(new Sample('temperature:3:11', 42, $to));
275 |
276 | $range = $this->sut->range(
277 | 'temperature:3:11'
278 | );
279 |
280 | $expectedRange = [
281 | Sample::createFromTimestamp(
282 | 'temperature:3:11',
283 | (float)30,
284 | DateTimeUtils::timestampWithMsFromDateTime(new DateTimeImmutable($from->format('Y-m-d H:i:s.u')))
285 | ),
286 | Sample::createFromTimestamp(
287 | 'temperature:3:11',
288 | (float)42,
289 | DateTimeUtils::timestampWithMsFromDateTime(new DateTimeImmutable($to->format('Y-m-d H:i:s.u')))
290 | ),
291 | ];
292 |
293 | self::assertEquals($expectedRange, $range);
294 | }
295 | }
296 |
--------------------------------------------------------------------------------
/tests/Unit/AggregationRuleTest.php:
--------------------------------------------------------------------------------
1 | expectException(InvalidAggregationException::class);
16 | new AggregationRule('foo', 1000);
17 | }
18 |
19 | public function testTypesAreCaseInsensitive(): void
20 | {
21 | $rule = new AggregationRule('avg', 1000);
22 | self::assertEquals(AggregationRule::AGG_AVG, $rule->getType());
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/Unit/DateTimeUtilsTest.php:
--------------------------------------------------------------------------------
1 | expectException(InvalidFilterOperationException::class);
16 | $this->expectExceptionMessage('Operation is not valid');
17 |
18 | $filter = new Filter('a', 'b');
19 | $filter->add('a', 666);
20 | }
21 |
22 | /**
23 | * @param mixed $operation
24 | * @dataProvider stringOperations
25 | */
26 | public function testOperationRequiresString($operation): void
27 | {
28 | $this->expectException(InvalidFilterOperationException::class);
29 | $this->expectExceptionMessage('The provided operation requires the value to be string');
30 |
31 | $filter = new Filter('a', 'b');
32 | $filter->add('a', $operation, []);
33 | }
34 |
35 | /**
36 | * @param mixed $operation
37 | * @dataProvider nullOperations
38 | */
39 | public function testOperationRequiresNull($operation): void
40 | {
41 | $this->expectException(InvalidFilterOperationException::class);
42 | $this->expectExceptionMessage('The provided operation requires the value to be null');
43 |
44 | $filter = new Filter('a', 'b');
45 | $filter->add('a', $operation, []);
46 | }
47 |
48 | /**
49 | * @param mixed $operation
50 | * @dataProvider arrayOperations
51 | */
52 | public function testOperationRequiresArray($operation): void
53 | {
54 | $this->expectException(InvalidFilterOperationException::class);
55 | $this->expectExceptionMessage('The provided operation requires the value to be an array');
56 |
57 | $filter = new Filter('a', 'b');
58 | $filter->add('a', $operation, null);
59 | }
60 |
61 | public function testToRedisParams(): void
62 | {
63 | $filter = new Filter('lab1', 'val1');
64 | $filter->add('lab2', Filter::OP_NOT_EQUALS, 'val2');
65 | $filter->add('lab3', Filter::OP_EXISTS);
66 | $filter->add('lab4', Filter::OP_NOT_EXISTS);
67 | $filter->add('lab5', Filter::OP_IN, ['a', 'b', 'c']);
68 | $filter->add('lab6', Filter::OP_NOT_IN, ['d', 'e', 'f']);
69 |
70 | $result = $filter->toRedisParams();
71 | $expected = ['lab1=val1', 'lab2!=val2', 'lab3=', 'lab4!=', 'lab5=(a,b,c)', 'lab6!=(d,e,f)'];
72 |
73 | self::assertEquals($expected, $result);
74 | }
75 |
76 | public function stringOperations(): array
77 | {
78 | return [[Filter::OP_EQUALS], [Filter::OP_NOT_EQUALS]];
79 | }
80 |
81 | public function nullOperations(): array
82 | {
83 | return [[Filter::OP_EXISTS], [Filter::OP_NOT_EXISTS]];
84 | }
85 |
86 | public function arrayOperations(): array
87 | {
88 | return [[Filter::OP_IN], [Filter::OP_NOT_IN]];
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/tests/Unit/MetadataTest.php:
--------------------------------------------------------------------------------
1 | getLastTimestamp());
28 | self::assertEquals(1, $metadata->getRetentionTime());
29 | self::assertEquals(2, $metadata->getChunkCount());
30 | self::assertEquals(3, $metadata->getMaxSamplesPerChunk());
31 | self::assertEquals($labels, $metadata->getLabels());
32 | self::assertEquals(null, $metadata->getSourceKey());
33 | self::assertEquals($rules, $metadata->getRules());
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tests/Unit/RedisClientTest.php:
--------------------------------------------------------------------------------
1 | createMock(Redis::class);
19 | $redisMock->expects(self::once())->method('isConnected')->willReturn(false);
20 | $redisMock->expects(self::once())->method('connect')->with(
21 | '127.0.0.1',
22 | 6379,
23 | 3,
24 | null,
25 | 1,
26 | 2.2
27 | );
28 | $redisMock->expects(self::once())->method('rawCommand')->with('MY', 'command');
29 | $connectionParams = new RedisConnectionParams();
30 | $connectionParams->setRetryInterval(1)
31 | ->setReadTimeout(2.2)
32 | ->setTimeout(3);
33 | $sut = new RedisClient($redisMock, $connectionParams);
34 | $sut->executeCommand(['MY', 'command']);
35 | }
36 |
37 | public function testPersistentConnection(): void
38 | {
39 | $redisMock = $this->createMock(Redis::class);
40 | $redisMock->expects(self::once())->method('isConnected')->willReturn(false);
41 | $redisMock->expects(self::once())->method('pconnect')->with(
42 | '127.0.0.1',
43 | 6379,
44 | 0,
45 | self::isType(IsType::TYPE_STRING),
46 | 0,
47 | 0.0
48 | );
49 | $connectionParams = new RedisConnectionParams();
50 | $connectionParams->setPersistentConnection(true);
51 | $sut = new RedisClient($redisMock, $connectionParams);
52 | $sut->executeCommand(['MY', 'command']);
53 | }
54 |
55 | public function testDontConnectIfNotNecessary(): void
56 | {
57 | $redisMock = $this->createMock(Redis::class);
58 | $redisMock->expects(self::once())->method('isConnected')->willReturn(true);
59 | $redisMock->expects(self::never())->method('connect');
60 | $redisMock->expects(self::never())->method('pconnect');
61 | $connectionParams = new RedisConnectionParams();
62 | $sut = new RedisClient($redisMock, $connectionParams);
63 | $sut->executeCommand(['MY', 'command']);
64 | }
65 |
66 | public function testFailureToConnectThrowsException(): void
67 | {
68 | $this->expectException(RedisClientException::class);
69 | $redisMock = $this->createMock(Redis::class);
70 | $redisMock->expects(self::once())->method('connect')->willReturn(false);
71 | $connectionParams = new RedisConnectionParams();
72 | $sut = new RedisClient($redisMock, $connectionParams);
73 | $sut->executeCommand(['MY', 'command']);
74 | }
75 |
76 | public function testConnectionWithPassword(): void
77 | {
78 | $redisMock = $this->createMock(Redis::class);
79 | $redisMock->expects(self::once())->method('isConnected')->willReturn(false);
80 | $redisMock->expects(self::once())
81 | ->method('auth')
82 | ->with('pass');
83 | $connectionParams = new RedisConnectionParams('127.0.0.1', 6379, null, 'pass');
84 | $sut = new RedisClient($redisMock, $connectionParams);
85 | $sut->executeCommand(['MY', 'command']);
86 | }
87 |
88 | public function testConnectionWithUseranameAndPassword(): void
89 | {
90 | $redisMock = $this->createMock(Redis::class);
91 | $redisMock->expects(self::once())->method('isConnected')->willReturn(false);
92 | $redisMock->expects(self::exactly(2))
93 | ->method('rawCommand')
94 | ->withConsecutive(
95 | ['AUTH', 'username', 'pass'],
96 | ['MY', 'command']
97 | );
98 | $connectionParams = new RedisConnectionParams('127.0.0.1', 6379, 'username', 'pass');
99 | $sut = new RedisClient($redisMock, $connectionParams);
100 | $sut->executeCommand(['MY', 'command']);
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/tests/Unit/SampleTest.php:
--------------------------------------------------------------------------------
1 | getTimestampWithMs();
16 | self::assertEquals(1483300866234, $ts);
17 | }
18 |
19 | public function testCreateFromTimestamp(): void
20 | {
21 | $sample = Sample::createFromTimestamp('a', 1, 1483300866234);
22 | $dateTime = $sample->getDateTime();
23 | self::assertEquals(new DateTimeImmutable('2017-01-01T20.01.06.234'), $dateTime);
24 | }
25 |
26 | public function testCurrentTimestampReturnsStar(): void
27 | {
28 | $sample = new Sample('a', 1);
29 | $params = $sample->toRedisParams();
30 | self::assertEquals(['a', '*', 1], $params);
31 | }
32 |
33 | public function testToRedisParams(): void
34 | {
35 | $sample = Sample::createFromTimestamp('a', 1, 1483300866234);
36 | $params = $sample->toRedisParams();
37 | self::assertEquals(['a', 1483300866234, 1], $params);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/Unit/SampleWithLabelsTest.php:
--------------------------------------------------------------------------------
1 | getLabels());
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/Unit/TimeSeriesTest.php:
--------------------------------------------------------------------------------
1 | redisClientMock = $this->createMock(RedisClient::class);
34 | $this->sut = new TimeSeries($this->redisClientMock);
35 | }
36 |
37 | /**
38 | * @dataProvider createDataProvider
39 | */
40 | public function testCreate(array $params, array $expectedParams): void
41 | {
42 | $this->redisClientMock
43 | ->expects(self::once())
44 | ->method('executeCommand')
45 | ->with($expectedParams);
46 | $this->sut->create(...$params);
47 | }
48 |
49 | public function createDataProvider(): array
50 | {
51 | return [
52 | 'full' => [
53 | [
54 | 'a',
55 | 10,
56 | [new Label('l1', 'v1'), new Label('l2', 'v2')],
57 | true,
58 | 10000,
59 | TimeSeries::DUPLICATE_POLICY_SUM
60 | ],
61 | [
62 | 'TS.CREATE',
63 | 'a',
64 | 'RETENTION',
65 | 10,
66 | 'UNCOMPRESSED',
67 | 'CHUNK_SIZE',
68 | '10000',
69 | 'DUPLICATE_POLICY',
70 | 'SUM',
71 | 'LABELS',
72 | 'l1',
73 | 'v1',
74 | 'l2',
75 | 'v2'
76 | ],
77 | ],
78 | 'most common' => [
79 | [
80 | 'a',
81 | 10,
82 | [new Label('l1', 'v1'), new Label('l2', 'v2')]
83 | ],
84 | ['TS.CREATE', 'a', 'RETENTION', 10, 'LABELS', 'l1', 'v1', 'l2', 'v2']
85 | ],
86 | 'no labels' => [['a', 10], ['TS.CREATE', 'a', 'RETENTION', 10]],
87 | 'minimal' => [['a'], ['TS.CREATE', 'a']]
88 | ];
89 | }
90 |
91 | public function testAlter(): void
92 | {
93 | $this->redisClientMock
94 | ->expects(self::once())
95 | ->method('executeCommand')
96 | ->with(['TS.ALTER', 'a', 'RETENTION', 10, 'LABELS', 'l1', 'v1', 'l2', 'v2']);
97 | $this->sut->alter(
98 | 'a',
99 | 10,
100 | [new Label('l1', 'v1'), new Label('l2', 'v2')]
101 | );
102 | }
103 |
104 | /**
105 | * @dataProvider addDataProvider
106 | */
107 | public function testAdd(array $params, array $expectedParams): void
108 | {
109 | $this->redisClientMock
110 | ->expects(self::once())
111 | ->method('executeCommand')
112 | ->with($expectedParams)
113 | ->willReturn(1483300866234);
114 | $addedSample = $this->sut->add(...$params);
115 | $expectedSample = new Sample('a', 10.1, new DateTimeImmutable('2017-01-01T20.01.06.234'));
116 | self::assertEquals($expectedSample, $addedSample);
117 | }
118 |
119 | public function addDataProvider(): array
120 | {
121 | return [
122 | 'full' => [
123 | [
124 | new Sample('a', 10.1, new DateTimeImmutable('2017-01-01T20.01.06.234')),
125 | 10,
126 | [new Label('l1', 'v1'), new Label('l2', 'v2')]
127 | ],
128 | ['TS.ADD', 'a', 1483300866234, 10.1, 'RETENTION', 10, 'LABELS', 'l1', 'v1', 'l2', 'v2']
129 | ],
130 | 'no datetime' => [
131 | [
132 | new Sample('a', 10.1),
133 | 10,
134 | [new Label('l1', 'v1'), new Label('l2', 'v2')]
135 | ],
136 | ['TS.ADD', 'a', '*', 10.1, 'RETENTION', 10, 'LABELS', 'l1', 'v1', 'l2', 'v2']
137 | ]
138 | ];
139 | }
140 |
141 | public function testAddMany(): void
142 | {
143 | $this->redisClientMock
144 | ->expects(self::once())
145 | ->method('executeCommand')
146 | ->with(['TS.MADD', 'a', '*', 10.1, 'b', 1483300866234, 1.0])
147 | ->willReturn([1483300866233, 1483300866234]);
148 | $addedSamples = $this->sut->addMany([
149 | new Sample('a', 10.1),
150 | new Sample('b', 1, new DateTimeImmutable('2017-01-01T20.01.06.234'))
151 | ]);
152 | $expectedSamples = [
153 | new Sample('a', 10.1, new DateTimeImmutable('2017-01-01T20.01.06.233')),
154 | new Sample('b', 1.0, new DateTimeImmutable('2017-01-01T20.01.06.234'))
155 | ];
156 | self::assertEquals($expectedSamples, $addedSamples);
157 | }
158 |
159 | public function testAddManyEmpty(): void
160 | {
161 | $this->redisClientMock
162 | ->expects(self::never())
163 | ->method('executeCommand')
164 | ->willReturn([]);
165 | $addedSamples = $this->sut->addMany([]);
166 | self::assertEquals([], $addedSamples);
167 | }
168 |
169 | public function testIncrementBy(): void
170 | {
171 | $this->redisClientMock
172 | ->expects(self::once())
173 | ->method('executeCommand')
174 | ->with(['TS.INCRBY', 'a', 10.1, 'RESET', 10, 'RETENTION', 20, 'LABELS', 'l1', 'v1', 'l2', 'v2']);
175 | $this->sut->incrementBy(
176 | new Sample('a', 10.1),
177 | 10,
178 | 20,
179 | [new Label('l1', 'v1'), new Label('l2', 'v2')]
180 | );
181 | }
182 |
183 | public function testDecrementBy(): void
184 | {
185 | $this->redisClientMock
186 | ->expects(self::once())
187 | ->method('executeCommand')
188 | ->with(['TS.DECRBY', 'a', 10.1, 'RESET', 10, 'TIMESTAMP', 1483300866234, 'RETENTION', 20, 'LABELS', 'l1', 'v1', 'l2', 'v2'])
189 | ->willReturn(1483300866234);
190 | $this->sut->decrementBy(
191 | new Sample('a', 10.1, new DateTimeImmutable('2017-01-01T20.01.06.234')),
192 | 10,
193 | 20,
194 | [new Label('l1', 'v1'), new Label('l2', 'v2')]
195 | );
196 | }
197 |
198 | public function testCreateRule(): void
199 | {
200 | $this->redisClientMock
201 | ->expects(self::once())
202 | ->method('executeCommand')
203 | ->with(['TS.CREATERULE', 'a', 'b', 'AGGREGATION', 'AVG', 100])
204 | ->willReturn(1483300866234);
205 | $this->sut->createRule('a', 'b', new AggregationRule(AggregationRule::AGG_AVG, 100));
206 | }
207 |
208 | public function testDeleteRule(): void
209 | {
210 | $this->redisClientMock
211 | ->expects(self::once())
212 | ->method('executeCommand')
213 | ->with(['TS.DELETERULE', 'a', 'b'])
214 | ->willReturn(1483300866234);
215 | $this->sut->deleteRule('a', 'b');
216 | }
217 |
218 | /**
219 | * @dataProvider rangeDataProvider
220 | */
221 | public function testRange(array $params, array $expectedRedisParam): void
222 | {
223 | $this->redisClientMock
224 | ->expects(self::once())
225 | ->method('executeCommand')
226 | ->with($expectedRedisParam)
227 | ->willReturn([[1483300866234, '9.1'], [1522923630234, '9.2']]);
228 | $returnedSamples = $this->sut->range(...$params);
229 | $expectedSamples = [
230 | new Sample('a', 9.1, new DateTimeImmutable('2017-01-01T20.01.06.234')),
231 | new Sample('a', 9.2, new DateTimeImmutable('2018-04-05T10.20.30.234'))
232 | ];
233 |
234 | self::assertEquals($expectedSamples, $returnedSamples);
235 | }
236 |
237 | public function rangeDataProvider(): array
238 | {
239 | return [
240 | 'full data' => [[
241 | 'a',
242 | new DateTimeImmutable('2017-01-01T20.01.06.234'),
243 | new DateTimeImmutable('2018-04-05T10.20.30.234'),
244 | 100,
245 | new AggregationRule(AggregationRule::AGG_LAST, 200)
246 | ], [
247 | 'TS.RANGE', 'a', 1483300866234, 1522923630234, 'COUNT', 100, 'AGGREGATION', 'LAST', 200
248 | ]],
249 | 'missing from' => [[
250 | 'a',
251 | null,
252 | new DateTimeImmutable('2018-04-05T10.20.30.234'),
253 | 100,
254 | new AggregationRule(AggregationRule::AGG_LAST, 200)
255 | ], [
256 | 'TS.RANGE', 'a', '-', 1522923630234, 'COUNT', 100, 'AGGREGATION', 'LAST', 200
257 | ]],
258 | 'missing from and to' => [[
259 | 'a',
260 | null,
261 | null,
262 | 100,
263 | new AggregationRule(AggregationRule::AGG_LAST, 200)
264 | ], [
265 | 'TS.RANGE', 'a', '-', '+', 'COUNT', 100, 'AGGREGATION', 'LAST', 200
266 | ]],
267 | 'missing from, to and count' => [[
268 | 'a',
269 | null,
270 | null,
271 | null,
272 | new AggregationRule(AggregationRule::AGG_LAST, 200)
273 | ], [
274 | 'TS.RANGE', 'a', '-', '+', 'AGGREGATION', 'LAST', 200
275 | ]],
276 | 'minimal' => [['a'], ['TS.RANGE', 'a', '-', '+']]
277 | ];
278 | }
279 |
280 | public function testInfo(): void
281 | {
282 | $this->redisClientMock
283 | ->expects(self::once())
284 | ->method('executeCommand')
285 | ->with(['TS.INFO', 'a'])
286 | ->willReturn([
287 | 'lastTimestamp',
288 | 1522923630234,
289 | 'retentionTime',
290 | 100,
291 | 'chunkCount',
292 | 10,
293 | 'maxSamplesPerChunk',
294 | 360,
295 | 'labels',
296 | [['a', 'a1'], ['b', 'b1']],
297 | 'sourceKey',
298 | null,
299 | 'rules',
300 | [['aa', 10, 'AVG']]
301 | ]);
302 | $returned = $this->sut->info('a');
303 | $expected = new Metadata(
304 | new DateTimeImmutable('2018-04-05T10.20.30.234'),
305 | 100,
306 | 10,
307 | 360,
308 | [new Label('a', 'a1'), new Label('b', 'b1')],
309 | null,
310 | ['aa' => new AggregationRule(AggregationRule::AGG_AVG, 10)]
311 | );
312 |
313 | self::assertEquals($expected, $returned);
314 | }
315 |
316 | public function testGetLastSample(): void
317 | {
318 | $this->redisClientMock
319 | ->expects(self::once())
320 | ->method('executeCommand')
321 | ->with(['TS.GET', 'a'])
322 | ->willReturn([1483300866234, '7']);
323 | $response = $this->sut->getLastSample('a');
324 | $expected = new Sample('a', 7.0, new DateTimeImmutable('2017-01-01T20.01.06.234'));
325 | self::assertEquals($expected, $response);
326 | }
327 |
328 | public function testGetLastSamples(): void
329 | {
330 | $this->redisClientMock
331 | ->expects(self::once())
332 | ->method('executeCommand')
333 | ->with(['TS.MGET', 'FILTER', 'a=a1'])
334 | ->willReturn([
335 | ['a', [['a', 'a1'], ['b', 'b1']], 1483300866234, '7'],
336 | ['b', [['a', 'a1'], ['c', 'c1']], 1522923630234, '7.1'],
337 | ]);
338 | $response = $this->sut->getLastSamples(new Filter('a', 'a1'));
339 | $expected = [
340 | new Sample('a', 7.0, new DateTimeImmutable('2017-01-01T20.01.06.234')),
341 | new Sample('b', 7.1, new DateTimeImmutable('2018-04-05T10.20.30.234')),
342 | ];
343 | self::assertEquals($expected, $response);
344 | }
345 |
346 | /**
347 | * @dataProvider multiRangeDataProvider
348 | */
349 | public function testMultiRange(array $params, array $expectedRedisParams): void
350 | {
351 | $this->redisClientMock
352 | ->expects(self::once())
353 | ->method('executeCommand')
354 | ->with($expectedRedisParams)
355 | ->willReturn([
356 | ['a', [['a', 'a1']], [[1483300866234, '7']]],
357 | ['b', [['a', 'a1']], [[1522923630234, '7.1']]],
358 | ]);
359 | $response = $this->sut->multiRange(...$params);
360 | $expected = [
361 | new Sample('a', 7.0, new DateTimeImmutable('2017-01-01T20.01.06.234')),
362 | new Sample('b', 7.1, new DateTimeImmutable('2018-04-05T10.20.30.234')),
363 | ];
364 | self::assertEquals($expected, $response);
365 | }
366 |
367 | public function multiRangeDataProvider(): array
368 | {
369 | return [
370 | 'full data' => [
371 | [
372 | new Filter('a', 'a1'),
373 | new DateTimeImmutable('2017-01-01T20.01.06.234'),
374 | new DateTimeImmutable('2018-04-05T10.20.30.234'),
375 | 100,
376 | new AggregationRule(AggregationRule::AGG_LAST, 200)
377 | ],
378 | ['TS.MRANGE', 1483300866234, 1522923630234, 'COUNT', 100, 'AGGREGATION', 'LAST', 200, 'FILTER', 'a=a1']
379 | ],
380 | 'missing dates' => [
381 | [
382 | new Filter('a', 'a1'),
383 | null,
384 | null,
385 | 100,
386 | new AggregationRule(AggregationRule::AGG_LAST, 200)
387 | ],
388 | ['TS.MRANGE', '-', '+', 'COUNT', 100, 'AGGREGATION', 'LAST', 200, 'FILTER', 'a=a1']
389 | ],
390 | 'missing dates and count' => [
391 | [
392 | new Filter('a', 'a1'),
393 | null,
394 | null,
395 | null,
396 | new AggregationRule(AggregationRule::AGG_LAST, 200)
397 | ],
398 | ['TS.MRANGE', '-', '+', 'AGGREGATION', 'LAST', 200, 'FILTER', 'a=a1']
399 | ],
400 | 'minimal' => [
401 | [new Filter('a', 'a1')],
402 | ['TS.MRANGE', '-', '+', 'FILTER', 'a=a1']
403 | ]
404 | ];
405 | }
406 |
407 | public function testGetKeysByFilter(): void
408 | {
409 | $keys = ['a', 'b'];
410 | $this->redisClientMock
411 | ->expects(self::once())
412 | ->method('executeCommand')
413 | ->with(['TS.QUERYINDEX', 'a=a1'])
414 | ->willReturn($keys);
415 | $response = $this->sut->getKeysByFilter(new Filter('a', 'a1'));
416 | self::assertEquals($keys, $response);
417 | }
418 | }
419 |
--------------------------------------------------------------------------------