Compare commits

..

18 Commits

Author SHA1 Message Date
Till Krüss f49e13ee3a tag stable 2.4.0 2025-04-30 08:16:02 -07:00
Vladyslav Vildanov 6e3e2c0e78 [2.x] 8.0-RC2 testing (#1532) 2025-04-29 08:25:13 -07:00
Vladyslav Vildanov c35c422eda [2.x] Fix the problem of protocol loss during cluster MOVED/ASK (#1530)
Co-authored-by: Lynn Club <lynncho@live.com>
Co-authored-by: Lynn Club <10293848+lynnclub@users.noreply.github.com>
Co-authored-by: Till Krüss <tillkruss@users.noreply.github.com>
2025-04-17 08:49:19 -07:00
Till Krüss 1b5ed7d516 ZPOPMIN & ZPOPMAX prefix tests 2025-04-15 13:25:42 -07:00
vladvildanov 5df852f227 Added tests cases 2025-04-15 09:16:08 -07:00
Till Krüss 9418df1924 fix changelog branch check 2025-04-15 09:15:52 -07:00
Till Krüss 0e4b3829a7 fix changelog check 2025-04-15 09:07:26 -07:00
Till Krüss 65f6127fab add changelog check 2025-04-15 08:54:08 -07:00
Till Krüss 7ff24b19ae Merge pull request #1528 from predis/vv-2.x-readme-8.0-support
(2.x) Update README.md (Redis 8.0 support)
2025-04-14 09:11:07 -07:00
vladvildanov 2babfc91d7 Update README.md (Redis 8.0 support) 2025-04-14 18:42:11 +03:00
Till Krüss fdd52d0284 Tag v2.4.0-RC1 (#1523) 2025-03-24 10:07:15 -07:00
Vladyslav Vildanov 48a861053e Added new hash-field expiration commands (#1520)
* Added new hash-field expiration commands

* Changed argument default value

* Marked test as skipped

* Removed version restriction

* Updated CHANGELOG.md
2025-03-24 17:50:12 +02:00
messikiller 7bf1f6eb29 optimize redis cluster slotmap with compact slot range object (#1493)
Co-authored-by: heqiming <heqiming@kingsoft.com>
Co-authored-by: Till Krüss <tillkruss@users.noreply.github.com>
Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com>
2025-03-19 09:24:53 -07:00
Vladyslav Vildanov 067b051900 Added missing FT._LIST and BITFIELD_RO commands (#1521)
* Added missing FT._LIST and BITFIELD_RO commands

* Codestyle fixes
2025-03-19 09:57:52 +02:00
Till Krüss df4a318b80 remove performance rambling from FAQ 2025-03-18 12:50:00 -07:00
Vladyslav Vildanov a904a3a273 Backport #1517 (#1518)
* Backport #1517

* Added versions restrictions

* Removed Relay for cluster with coverage

* Added phpcov package
2025-03-05 19:29:38 +02:00
Vladyslav Vildanov 8b240d15a5 Backport #1512 (#1515)
* Backport #1512
* Removed outdated test
2025-02-25 09:12:10 -08:00
Vladyslav Vildanov 02e2a7e1cc Added test coverage for compatibility with Redis 8.0 (#1513)
* Added testing with 8.0

* Removed old service

* Moved checkout stage above image

* Changed service name

* Override config options for Redis < 7

* Exported to github env

* Print major version output

* Added double quotes

* Test fixes

* Merge cluster and standalone infrastructure

* Removed separate cluster job and merge together with standalone

* Merge stack tests into main test job

* Fixed annotation condition

* tweaks

* Moved docker-compose.yml to root folder

* Added compose file to expected

* fixed in v0.10.1

* move docker file

* Added -all profile in docker-compose

* Removed redis official image, reduce test matrix, moved docker-compose.yml

* Revert docker-compose moving

* Added test coverage to verify compatibility with 8.0

* Mark tests as relay-incompatible

* Codestyle fix

* Marked as relay-incompatible

---------

Co-authored-by: Till Krüss <tillkruss@users.noreply.github.com>
2025-02-24 09:22:45 -08:00
64 changed files with 2791 additions and 250 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ services:
- standalone
redis-cluster:
image: ${REDIS_CLUSTER_IMAGE_NAME:-redislabs/client-libs-test:7.4.2}
image: ${REDIS_IMAGE_NAME:-redislabs/client-libs-test:7.4.2}
container_name: redis-cluster
environment:
- REDIS_CLUSTER=yes
+29
View File
@@ -165,3 +165,32 @@ jobs:
- name: Search for misspellings
run: $(python -m site --user-base)/bin/codespell
changelog:
name: Changelog
runs-on: ubuntu-latest
timeout-minutes: 5
if: >-
github.event_name == 'pull_request' &&
!contains(github.event.head_commit.message, 'nochangelog') &&
!contains(github.event.head_commit.message, 'no-changelog') &&
!contains(github.event.head_commit.message, 'no changelog') &&
!contains(github.event.pull_request.labels.*.name, 'no-changelog')
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for CHANGELOG entry
env:
TARGET: ${{ github.event.pull_request.base.ref }}
run: |
FILES_CHANGED=$(git diff --name-only origin/$TARGET...HEAD | grep -E 'CHANGELOG\.md' -c)
if [ "$FILES_CHANGED" != "1" ]; then
echo "CHANGELOG.md was not updated";
exit 1;
fi;
+64 -19
View File
@@ -38,14 +38,22 @@ jobs:
run: |
# Mapping of original redis versions to client test containers
declare -A redis_clients_version_mapping=(
["8.0"]="8.0-M04-pre"
["8.0"]="8.0-RC2-pre"
["7.4"]="7.4.2"
["7.2"]="7.2.7"
["6.2"]="6.2.17"
)
# Mapping of redis version to stack version
declare -A redis_stack_version_mapping=(
["7.4"]="rs-7.4.0-v3"
["7.2"]="rs-7.2.0-v15"
["6.2"]="rs-6.2.6-v19"
)
if [[ -v redis_clients_version_mapping[${{ matrix.redis }}] ]]; then
echo "REDIS_IMAGE_NAME=redislabs/client-libs-test:${redis_clients_version_mapping[${{ matrix.redis }}]}" >> $GITHUB_ENV
echo "REDIS_STACK_IMAGE_NAME=redislabs/client-libs-test:${redis_stack_version_mapping[${{ matrix.redis }}]}" >> $GITHUB_ENV
echo "DOCKER_SERVICE=redis-clients" >> $GITHUB_ENV
redis_major_version=$(echo "${{ matrix.redis }}" | grep -oP '^\d+')
@@ -53,7 +61,11 @@ jobs:
# Some configuration options available since Redis > 7
if (( redis_major_version < 7 )); then
echo "REDIS_EXTRA_ARGS="--tls-auth-clients optional --save ''"" >> $GITHUB_ENV
else
# Since 8.0 modules are bundled with core
echo "REDIS_STACK_SERVER_PORT=6379" >> $GITHUB_ENV
fi
else
echo "REDIS_IMAGE_NAME=redis:${{ matrix.redis }}" >> $GITHUB_ENV
echo "DOCKER_SERVICE=redis-official" >> $GITHUB_ENV
@@ -69,15 +81,17 @@ jobs:
services: ${{ env.DOCKER_SERVICE }}
- name: Start Redis stack image
id: stack_infra
uses: hoverkraft-tech/compose-action@v2.0.1
if: ${{ matrix.redis == '7.4' }}
if: ${{ matrix.redis >= '6.2' && matrix.redis < '8.0' }}
with:
compose-file: .github/docker-compose.yml
services: redis-stack
- name: Start Redis cluster image
id: cluster_infra
uses: hoverkraft-tech/compose-action@v2.0.1
if: ${{ matrix.redis == '7.4' }}
if: ${{ matrix.redis > '4.0' }}
with:
compose-file: .github/docker-compose.yml
services: redis-cluster
@@ -87,7 +101,7 @@ jobs:
with:
php-version: ${{ matrix.php }}
extensions: relay
coverage: ${{ (matrix.php == '8.1' && matrix.redis == '7.4') && 'xdebug' || 'none' }}
coverage: ${{ (matrix.php == '8.4' && matrix.redis == '7.4') && 'xdebug' || 'none' }}
- name: Install Composer dependencies
uses: ramsey/composer-install@v2
@@ -96,43 +110,74 @@ jobs:
composer-options: ${{ matrix.php == '8.0' && '--ignore-platform-reqs' || '' }}
- name: Run tests
if: ${{ matrix.php != '8.1' || matrix.redis != '7.4' }}
if: ${{ matrix.php != '8.4' || matrix.redis != '7.4' }}
run: vendor/bin/phpunit
- name: Run tests with coverage
if: ${{ matrix.php == '8.1' && matrix.redis == '7.4' }}
run: vendor/bin/phpunit --coverage-clover build/logs/clover-default.xml --coverage-filter ./src
if: ${{ matrix.php == '8.4' && matrix.redis == '7.4' }}
run: vendor/bin/phpunit --coverage-php build/cov/coverage-predis.cov --coverage-filter ./src
- name: Run tests using Relay
if: ${{ matrix.php != '8.4' && matrix.redis >= '6.2' }}
run: vendor/bin/phpunit -c phpunit.relay.xml
- name: Run tests using Relay with coverage
if: ${{ matrix.php == '8.1' && matrix.redis == '7.4' }}
run: vendor/bin/phpunit -c phpunit.relay.xml --coverage-clover build/logs/clover-relay.xml --coverage-filter ./src
if: ${{ matrix.php == '8.4' && matrix.redis == '7.4' }}
run: vendor/bin/phpunit -c phpunit.relay.xml --coverage-php build/cov/coverage-relay.cov --coverage-filter ./src
- name: Run stack tests
if: ${{ matrix.redis == '7.4' }}
if: ${{ (matrix.php != '8.4' || matrix.redis != '7.4') && matrix.redis >= '6.2' }}
run: vendor/bin/phpunit --group realm-stack
- name: Run stack tests with coverage
if: ${{ matrix.php == '8.4' && matrix.redis == '7.4' && steps.stack_infra.conclusion == 'success' }}
run: vendor/bin/phpunit --group realm-stack --coverage-php build/cov/coverage-stack.cov --coverage-filter ./src
env:
REDIS_STACK_SERVER_PORT: 6479
- name: Run stack tests using Relay
if: ${{ matrix.redis == '7.4' }}
if: ${{ matrix.php != '8.4' && matrix.redis == '7.4' }}
run: vendor/bin/phpunit --group realm-stack -c phpunit.relay.xml
- name: Run stack tests using Relay with coverage
if: ${{ matrix.php == '8.4' && matrix.redis == '7.4' }}
run: vendor/bin/phpunit --group realm-stack -c phpunit.relay.xml --coverage-php build/cov/coverage-stack-relay.cov --coverage-filter ./src
env:
REDIS_STACK_SERVER_PORT: 6479
- name: Run tests against cluster
if: ${{ matrix.redis == '7.4' }}
if: ${{ (matrix.php != '8.4' || matrix.redis != '7.4') && steps.cluster_infra.conclusion == 'success' }}
run: |
vendor/bin/phpunit --group cluster
- name: Run tests against cluster with coverage
if: ${{ matrix.php == '8.4' && matrix.redis == '7.4' && steps.cluster_infra.conclusion == 'success' }}
run: |
vendor/bin/phpunit --group cluster --coverage-php build/cov/coverage-cluster.cov --coverage-filter ./src
- name: Run tests against cluster using Relay
if: ${{ matrix.redis == '7.4' }}
if: ${{ matrix.php != '8.4' && matrix.redis == '7.4' }}
run: |
vendor/bin/phpunit -c phpunit.relay.xml --group cluster
- name: Merge coverage reports
if: ${{ matrix.php == '8.4' && matrix.redis == '7.4' }}
run: php vendor/bin/phpcov merge --clover build/logs/clover.xml build/cov
- name: Send coverage to Coveralls
env:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
if: ${{ env.COVERALLS_REPO_TOKEN && matrix.php == '8.1' && matrix.redis == '7.4' }}
run: |
wget "https://github.com/php-coveralls/php-coveralls/releases/download/v2.5.3/php-coveralls.phar"
php ./php-coveralls.phar -v
uses: coverallsapp/github-action@v2
if: ${{ matrix.php == '8.4' && matrix.redis == '7.4' }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel: true
finish:
name: Finish Coverall
needs: predis
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
uses: coverallsapp/github-action@v2
with:
parallel-finished: true
+19 -2
View File
@@ -1,8 +1,25 @@
## Changelog
## Unreleased
## v2.4.0 (2025-04-30)
### Added
- Added new hash-field expiration commands (#1520)
- Added missing `FT._LIST` and `BITFIELD_RO` commands (#1521)
### Changed
- Update `WATCH` command to accept `string|string[]` (#1476)
- Optimize cluster slotmap with compact slot range object (#1493)
### Fixed
- Fixed PHP 8.4 compatibility with `stream_context_set_option()`
- Fixed `EVAL_RO` cluster support (#1449)
- Fixed PHP 8.4 compatibility with `stream_context_set_option()` (#1503)
- Prevent named arguments runtime failure (#1509)
- Mark `GEOSEARCH` as read-only to ensure execution on replica (#1481)
- Fixed protocol loss during redis cluster `MOVED` / `ASK` (#1530)
### Maintenance
- Added CI testing with Redis 8.0 (#1510)
- Added test coverage for compatibility with Redis 8.0 (#1513)
- Use parallel on PHP-CS-Fixer (#1489)
## v2.3.0 (2024-11-21)
### Added
+9 -57
View File
@@ -81,65 +81,17 @@ An exception to this rule is [`SORT`](http://redis.io/commands/sort) for which m
## When should I use Relay? ##
If you care about performance, __always__. [Relay](https://github.com/cachewerk/relay) is free to use.
If you care about performance, __always__. [Relay][relay] is free to use.
## When should I use PhpRedis? ###
Predis is fast enough when Redis is located on the same machine as PHP, more on that later.
Predis is fast enough when Redis is located on the same machine as PHP.
[PhpRedis](https://github.com/phpredis/phpredis) (and Relay) perform significantly better when
network I/O is involved, due to their ability to compress data by ~75%. Fewer bytes and received
sent over the network [means faster operations](https://akalongman.medium.com/phpredis-vs-predis-comparison-on-real-production-data-a819b48cbadb),
and potentially cost savings when network traffic isn't free (e.g. AWS ElastiCache Inter-AZ transfer costs).
[PhpRedis][phpredis] and [Relay][relay] perform significantly better when network I/O is involved,
due to its ability to compress data by ~75%. Fewer bytes and received sent over the network
[means faster operations][performance], and potentially cost savings when network traffic isn't
free (e.g. AWS ElastiCache Inter-AZ transfer costs).
## Predis is a pure-PHP implementation: it can not be fast enough! ##
It really depends, but most of the times the answer is: _yes, it is fast enough_. I will give you a
couple of easy numbers with a simple test that uses a single client and is executed by PHP 5.5.6
against a local instance of Redis 2.8 that runs under Ubuntu 13.10 on a Intel Q6600:
```
21000 SET/sec using 12 bytes for both key and value.
21000 GET/sec while retrieving the very same values.
0.130 seconds to fetch 30000 keys using _KEYS *_.
```
How does it compare with [__PhpRedis__](http://github.com/phpredis/phpredis), a nice C extension
providing an efficient client for Redis?
```
30100 SET/sec using 12 bytes for both key and value
29400 GET/sec while retrieving the very same values
0.035 seconds to fetch 30000 keys using "KEYS *"".
```
Wow __PhpRedis__ seems much faster! Well, we are comparing a C extension with a pure-PHP library so
lower numbers are quite expected but there is a fundamental flaw in them: is this really how you are
going to use Redis in your application? Are you really going to send thousands of commands using a
for-loop on each page request using a single client instance? If so... well I guess you are probably
doing something wrong. Also, if you need to `SET` or `GET` multiple keys you should definitely use
commands such as `MSET` and `MGET`. You can also use pipelining to get more performances when this
technique can be used.
There is one more thing: we have tested the overhead of Predis by connecting on a localhost instance
of Redis but how these numbers change when we hit the physical network by connecting to remote Redis
instances?
```
Using Predis:
3200 SET/sec using 12 bytes for both key and value
3200 GET/sec while retrieving the very same values
0.132 seconds to fetch 30000 keys using "KEYS *".
Using PhpRedis:
3500 SET/sec using 12 bytes for both key and value
3500 GET/sec while retrieving the very same values
0.045 seconds to fetch 30000 keys using "KEYS *".
```
There you go, you get almost the same average numbers and the reason is simple: network latency is a
real performance killer and you cannot do (almost) anything about that. As a disclaimer, remember
that we are measuring the overhead of client libraries implementations and the effects of network
round-trip times, so we are not really measuring how fast Redis is. Redis shines best with thousands
of concurrent clients doing requests! Also, actual performances should be measured according to how
your application will use Redis.
[phpredis]: https://github.com/phpredis/phpredis
[relay]: [https://github.com/phpredis/phpredis](https://github.com/cachewerk/relay)
[performance]: https://akalongman.medium.com/phpredis-vs-predis-comparison-on-real-production-data-a819b48cbadb
+1 -1
View File
@@ -14,7 +14,7 @@ More details about this project can be found on the [frequently asked questions]
## Main features ##
- Support for Redis from __3.0__ to __7.4__.
- Support for Redis from __3.0__ to __8.0__.
- Support for clustering using client-side sharding and pluggable keyspace distributors.
- Support for [redis-cluster](http://redis.io/topics/cluster-tutorial) (Redis >= 3.0).
- Support for master-slave replication setups and [redis-sentinel](http://redis.io/topics/sentinel).
+1 -1
View File
@@ -1 +1 @@
2.3.1-dev
2.4.0
+2 -1
View File
@@ -27,7 +27,8 @@
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.3",
"phpstan/phpstan": "^1.9",
"phpunit/phpunit": "^8.0 || ^9.4"
"phpunit/phpunit": "^8.0 || ^9.4",
"phpunit/phpcov": "^6.0 || ^8.0"
},
"suggest": {
"ext-relay": "Faster connection with in-memory caching (>=0.6.2)"
+1 -1
View File
@@ -38,9 +38,9 @@
<php>
<const name="REDIS_SERVER_HOST" value="127.0.0.1" />
<const name="REDIS_SERVER_PORT" value="6379" />
<const name="REDIS_STACK_SERVER_PORT" value="6479" />
<const name="REDIS_SERVER_DBNUM" value="0" />
<env name="USE_RELAY" value="true" />
<env name="REDIS_STACK_SERVER_PORT" value="6479" />
<!-- Redis Cluster -->
<!-- Only master nodes endpoints included -->
+1 -1
View File
@@ -50,9 +50,9 @@
<php>
<const name="REDIS_SERVER_HOST" value="127.0.0.1" />
<const name="REDIS_SERVER_PORT" value="6379" />
<const name="REDIS_STACK_SERVER_PORT" value="6479" />
<const name="REDIS_SERVER_DBNUM" value="0" />
<env name="USE_RELAY" value="false" />
<env name="REDIS_STACK_SERVER_PORT" value="6479" />
<!-- Redis Cluster -->
<!-- Only master nodes endpoints included -->
+1 -1
View File
@@ -53,7 +53,7 @@ use Traversable;
*/
class Client implements ClientInterface, IteratorAggregate
{
public const VERSION = '2.3.1-dev';
public const VERSION = '2.4.0';
/** @var OptionsInterface */
private $options;
+7
View File
@@ -44,6 +44,8 @@ use Predis\Command\Redis\Container\FunctionContainer;
use Predis\Command\Redis\Container\Json\JSONDEBUG;
use Predis\Command\Redis\Container\Search\FTCONFIG;
use Predis\Command\Redis\Container\Search\FTCURSOR;
use Predis\Command\Redis\HGETEX;
use Predis\Command\Redis\HSETEX;
/**
* Interface defining a client-side context such as a pipeline or transaction.
@@ -83,6 +85,7 @@ use Predis\Command\Redis\Container\Search\FTCURSOR;
* @method $this bitcount(string $key, $start = null, $end = null, string $index = 'byte')
* @method $this bitop($operation, $destkey, $key)
* @method $this bitfield($key, $subcommand, ...$subcommandArg)
* @method $this bitfield_ro(string $key, ?array $encodingOffsetMap = null)
* @method $this bitpos($key, $bit, $start = null, $end = null, string $index = 'byte')
* @method $this blmpop(int $timeout, array $keys, string $modifier = 'left', int $count = 1)
* @method $this bzpopmax(array $keys, int $timeout)
@@ -111,6 +114,7 @@ use Predis\Command\Redis\Container\Search\FTCURSOR;
* @method $this failover(?To $to = null, bool $abort = false, int $timeout = -1)
* @method $this fcall(string $function, array $keys, ...$args)
* @method $this fcall_ro(string $function, array $keys, ...$args)
* @method $this ft_list()
* @method $this ftaggregate(string $index, string $query, ?AggregateArguments $arguments = null)
* @method $this ftaliasadd(string $alias, string $index)
* @method $this ftaliasdel(string $alias)
@@ -162,7 +166,9 @@ use Predis\Command\Redis\Container\Search\FTCURSOR;
* @method $this hpexpireat(string $key, int $unixTimeMilliseconds, array $fields, string $flag = null)
* @method $this hpexpiretime(string $key, array $fields)
* @method $this hget($key, $field)
* @method $this hgetex(string $key, array $fields, string $modifier = HGETEX::NULL)
* @method $this hgetall($key)
* @method $this hgetdel(string $key, array $fields)
* @method $this hincrby($key, $field, $increment)
* @method $this hincrbyfloat($key, $field, $increment)
* @method $this hkeys($key)
@@ -172,6 +178,7 @@ use Predis\Command\Redis\Container\Search\FTCURSOR;
* @method $this hrandfield(string $key, int $count = 1, bool $withValues = false)
* @method $this hscan($key, $cursor, ?array $options = null)
* @method $this hset($key, $field, $value)
* @method $this hsetex(string $key, array $fieldValueMap, string $setModifier = HSETEX::SET_NULL, string $ttlModifier = HSETEX::TTL_NULL, int|bool $ttlModifierValue = false)
* @method $this hsetnx($key, $field, $value)
* @method $this httl(string $key, array $fields)
* @method $this hpttl(string $key, array $fields)
+7
View File
@@ -45,6 +45,8 @@ use Predis\Command\Redis\Container\FunctionContainer;
use Predis\Command\Redis\Container\Json\JSONDEBUG;
use Predis\Command\Redis\Container\Search\FTCONFIG;
use Predis\Command\Redis\Container\Search\FTCURSOR;
use Predis\Command\Redis\HGETEX;
use Predis\Command\Redis\HSETEX;
use Predis\Configuration\OptionsInterface;
use Predis\Connection\ConnectionInterface;
use Predis\Response\Status;
@@ -92,6 +94,7 @@ use Predis\Response\Status;
* @method int bitcount(string $key, $start = null, $end = null, string $index = 'byte')
* @method int bitop($operation, $destkey, $key)
* @method array|null bitfield(string $key, $subcommand, ...$subcommandArg)
* @method array|null bitfield_ro(string $key, ?array $encodingOffsetMap = null)
* @method int bitpos(string $key, $bit, $start = null, $end = null, string $index = 'byte')
* @method array blmpop(int $timeout, array $keys, string $modifier = 'left', int $count = 1)
* @method array bzpopmax(array $keys, int $timeout)
@@ -120,6 +123,7 @@ use Predis\Response\Status;
* @method Status failover(?To $to = null, bool $abort = false, int $timeout = -1)
* @method mixed fcall(string $function, array $keys, ...$args)
* @method mixed fcall_ro(string $function, array $keys, ...$args)
* @method array ft_list()
* @method array ftaggregate(string $index, string $query, ?AggregateArguments $arguments = null)
* @method Status ftaliasadd(string $alias, string $index)
* @method Status ftaliasdel(string $alias)
@@ -171,7 +175,9 @@ use Predis\Response\Status;
* @method array|null hpexpireat(string $key, int $unixTimeMilliseconds, array $fields, string $flag = null)
* @method array|null hpexpiretime(string $key, array $fields)
* @method string|null hget(string $key, string $field)
* @method array|null hgetex(string $key, array $fields, string $modifier = HGETEX::NULL, int|bool $modifierValue = false)
* @method array hgetall(string $key)
* @method array hgetdel(string $key, array $fields)
* @method int hincrby(string $key, string $field, int $increment)
* @method string hincrbyfloat(string $key, string $field, int|float $increment)
* @method array hkeys(string $key)
@@ -181,6 +187,7 @@ use Predis\Response\Status;
* @method array hrandfield(string $key, int $count = 1, bool $withValues = false)
* @method array hscan(string $key, $cursor, ?array $options = null)
* @method int hset(string $key, string $field, string $value)
* @method int hsetex(string $key, array $fieldValueMap, string $setModifier = HSETEX::SET_NULL, string $ttlModifier = HSETEX::TTL_NULL, int|bool $ttlModifierValue = false)
* @method int hsetnx(string $key, string $field, string $value)
* @method array|null httl(string $key, array $fields)
* @method array|null hpttl(string $key, array $fields)
+40
View File
@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster;
/**
* Represents the gap between slot ranges.
*/
class NullSlotRange extends SlotRange
{
public function __construct(int $start, int $end)
{
parent::__construct($start, $end, '');
}
/**
* {@inheritDoc}
*/
public function toArray(): array
{
return [];
}
/**
* {@inheritDoc}
*/
public function count(): int
{
return 0;
}
}
+209
View File
@@ -0,0 +1,209 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster;
use ArrayAccess;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use OutOfBoundsException;
use Predis\Connection\NodeConnectionInterface;
use ReturnTypeWillChange;
use Traversable;
/**
* Slot map for redis-cluster.
*/
class SimpleSlotMap implements ArrayAccess, IteratorAggregate, Countable
{
private $slots = [];
/**
* Checks if the given slot is valid.
*
* @param int $slot Slot index.
*
* @return bool
*/
public static function isValid($slot)
{
return $slot >= 0x0000 && $slot <= 0x3FFF;
}
/**
* Checks if the given slot range is valid.
*
* @param int $first Initial slot of the range.
* @param int $last Last slot of the range.
*
* @return bool
*/
public static function isValidRange($first, $last)
{
return $first >= 0x0000 && $first <= 0x3FFF && $last >= 0x0000 && $last <= 0x3FFF && $first <= $last;
}
/**
* Resets the slot map.
*/
public function reset()
{
$this->slots = [];
}
/**
* Checks if the slot map is empty.
*
* @return bool
*/
public function isEmpty()
{
return empty($this->slots);
}
/**
* Returns the current slot map as a dictionary of $slot => $node.
*
* The order of the slots in the dictionary is not guaranteed.
*
* @return array
*/
public function toArray()
{
return $this->slots;
}
/**
* Returns the list of unique nodes in the slot map.
*
* @return array
*/
public function getNodes()
{
return array_keys(array_flip($this->slots));
}
/**
* Assigns the specified slot range to a node.
*
* @param int $first Initial slot of the range.
* @param int $last Last slot of the range.
* @param NodeConnectionInterface|string $connection ID or connection instance.
*
* @throws OutOfBoundsException
*/
public function setSlots($first, $last, $connection)
{
if (!static::isValidRange($first, $last)) {
throw new OutOfBoundsException("Invalid slot range $first-$last for `$connection`");
}
$this->slots += array_fill($first, $last - $first + 1, (string) $connection);
}
/**
* Returns the specified slot range.
*
* @param int $first Initial slot of the range.
* @param int $last Last slot of the range.
*
* @return array
*/
public function getSlots($first, $last)
{
if (!static::isValidRange($first, $last)) {
throw new OutOfBoundsException("Invalid slot range $first-$last");
}
return array_intersect_key($this->slots, array_fill($first, $last - $first + 1, null));
}
/**
* Checks if the specified slot is assigned.
*
* @param int $slot Slot index.
*
* @return bool
*/
#[ReturnTypeWillChange]
public function offsetExists($slot)
{
return isset($this->slots[$slot]);
}
/**
* Returns the node assigned to the specified slot.
*
* @param int $slot Slot index.
*
* @return string|null
*/
#[ReturnTypeWillChange]
public function offsetGet($slot)
{
return $this->slots[$slot] ?? null;
}
/**
* Assigns the specified slot to a node.
*
* @param int $slot Slot index.
* @param NodeConnectionInterface|string $connection ID or connection instance.
*
* @return void
*/
#[ReturnTypeWillChange]
public function offsetSet($slot, $connection)
{
if (!static::isValid($slot)) {
throw new OutOfBoundsException("Invalid slot $slot for `$connection`");
}
$this->slots[(int) $slot] = (string) $connection;
}
/**
* Returns the node assigned to the specified slot.
*
* @param int $slot Slot index.
*
* @return void
*/
#[ReturnTypeWillChange]
public function offsetUnset($slot)
{
unset($this->slots[$slot]);
}
/**
* Returns the current number of assigned slots.
*
* @return int
*/
#[ReturnTypeWillChange]
public function count()
{
return count($this->slots);
}
/**
* Returns an iterator over the slot map.
*
* @return Traversable<int, string>
*/
#[ReturnTypeWillChange]
public function getIterator()
{
return new ArrayIterator($this->slots);
}
}
+225 -17
View File
@@ -22,11 +22,16 @@ use ReturnTypeWillChange;
use Traversable;
/**
* Slot map for redis-cluster.
* Compact slot map for redis-cluster.
*/
class SlotMap implements ArrayAccess, IteratorAggregate, Countable
{
private $slots = [];
/**
* Slot ranges list.
*
* @var SlotRange[]
*/
private $slotRanges = [];
/**
* Checks if the given slot is valid.
@@ -37,7 +42,7 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable
*/
public static function isValid($slot)
{
return $slot >= 0x0000 && $slot <= 0x3FFF;
return $slot >= 0 && $slot <= SlotRange::MAX_SLOTS;
}
/**
@@ -50,7 +55,7 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable
*/
public static function isValidRange($first, $last)
{
return $first >= 0x0000 && $first <= 0x3FFF && $last >= 0x0000 && $last <= 0x3FFF && $first <= $last;
return SlotRange::isValidRange($first, $last);
}
/**
@@ -58,7 +63,7 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable
*/
public function reset()
{
$this->slots = [];
$this->slotRanges = [];
}
/**
@@ -68,7 +73,7 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable
*/
public function isEmpty()
{
return empty($this->slots);
return empty($this->slotRanges);
}
/**
@@ -80,7 +85,13 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable
*/
public function toArray()
{
return $this->slots;
return array_reduce(
$this->slotRanges,
function ($carry, $slotRange) {
return $carry + $slotRange->toArray();
},
[]
);
}
/**
@@ -90,7 +101,22 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable
*/
public function getNodes()
{
return array_keys(array_flip($this->slots));
return array_unique(array_map(
function ($slotRange) {
return $slotRange->getConnection();
},
$this->slotRanges
));
}
/**
* Returns the list of slot ranges.
*
* @return SlotRange[]
*/
public function getSlotRanges()
{
return $this->slotRanges;
}
/**
@@ -108,7 +134,31 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable
throw new OutOfBoundsException("Invalid slot range $first-$last for `$connection`");
}
$this->slots += array_fill($first, $last - $first + 1, (string) $connection);
$targetSlotRange = new SlotRange($first, $last, (string) $connection);
// Get gaps of slot ranges list.
$gaps = $this->getGaps($this->slotRanges);
$results = $this->slotRanges;
foreach ($gaps as $gap) {
if (!$gap->hasIntersectionWith($targetSlotRange)) {
continue;
}
// Get intersection of the gap and target slot range.
$results[] = new SlotRange(
max($gap->getStart(), $targetSlotRange->getStart()),
min($gap->getEnd(), $targetSlotRange->getEnd()),
$targetSlotRange->getConnection()
);
}
$this->sortSlotRanges($results);
$results = $this->compactSlotRanges($results);
$this->slotRanges = $results;
}
/**
@@ -117,7 +167,7 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable
* @param int $first Initial slot of the range.
* @param int $last Last slot of the range.
*
* @return array
* @return array<int, string>
*/
public function getSlots($first, $last)
{
@@ -125,7 +175,28 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable
throw new OutOfBoundsException("Invalid slot range $first-$last");
}
return array_intersect_key($this->slots, array_fill($first, $last - $first + 1, null));
$placeHolder = new NullSlotRange($first, $last);
$intersections = [];
foreach ($this->slotRanges as $slotRange) {
if (!$placeHolder->hasIntersectionWith($slotRange)) {
continue;
}
$intersections[] = new SlotRange(
max($placeHolder->getStart(), $slotRange->getStart()),
min($placeHolder->getEnd(), $slotRange->getEnd()),
$slotRange->getConnection()
);
}
return array_reduce(
$intersections,
function ($carry, $slotRange) {
return $carry + $slotRange->toArray();
},
[]
);
}
/**
@@ -138,7 +209,7 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable
#[ReturnTypeWillChange]
public function offsetExists($slot)
{
return isset($this->slots[$slot]);
return $this->findRangeBySlot($slot) !== false;
}
/**
@@ -151,7 +222,9 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable
#[ReturnTypeWillChange]
public function offsetGet($slot)
{
return $this->slots[$slot] ?? null;
$found = $this->findRangeBySlot($slot);
return $found ? $found->getConnection() : null;
}
/**
@@ -169,7 +242,8 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable
throw new OutOfBoundsException("Invalid slot $slot for `$connection`");
}
$this->slots[(int) $slot] = (string) $connection;
$this->offsetUnset($slot);
$this->setSlots($slot, $slot, $connection);
}
/**
@@ -182,7 +256,26 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable
#[ReturnTypeWillChange]
public function offsetUnset($slot)
{
unset($this->slots[$slot]);
if (!static::isValid($slot)) {
throw new OutOfBoundsException("Invalid slot $slot");
}
$results = [];
foreach ($this->slotRanges as $slotRange) {
if (!$slotRange->hasSlot($slot)) {
$results[] = $slotRange;
}
if (static::isValidRange($slotRange->getStart(), $slot - 1)) {
$results[] = new SlotRange($slotRange->getStart(), $slot - 1, $slotRange->getConnection());
}
if (static::isValidRange($slot + 1, $slotRange->getEnd())) {
$results[] = new SlotRange($slot + 1, $slotRange->getEnd(), $slotRange->getConnection());
}
}
$this->slotRanges = $results;
}
/**
@@ -193,7 +286,12 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable
#[ReturnTypeWillChange]
public function count()
{
return count($this->slots);
return array_sum(array_map(
function ($slotRange) {
return $slotRange->count();
},
$this->slotRanges
));
}
/**
@@ -204,6 +302,116 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable
#[ReturnTypeWillChange]
public function getIterator()
{
return new ArrayIterator($this->slots);
return new ArrayIterator($this->toArray());
}
/**
* Find the slot range which contains the specific slot index.
*
* @param int $slot Slot index.
*
* @return SlotRange|false The slot range object or false if not found.
*/
protected function findRangeBySlot(int $slot)
{
foreach ($this->slotRanges as $slotRange) {
if ($slotRange->hasSlot($slot)) {
return $slotRange;
}
}
return false;
}
/**
* Get gaps between sorted slot ranges with NullSlotRange object.
*
* @param SlotRange[] $slotRanges
*
* @return SlotRange[]
*/
protected function getGaps(array $slotRanges)
{
if (empty($slotRanges)) {
return [
new NullSlotRange(0, SlotRange::MAX_SLOTS),
];
}
$gaps = [];
$count = count($slotRanges);
$i = 0;
foreach ($slotRanges as $key => $slotRange) {
$start = $slotRange->getStart();
$end = $slotRange->getEnd();
if (static::isValidRange($i, $start - 1)) {
$gaps[] = new NullSlotRange($i, $start - 1);
}
$i = $end + 1;
if ($key === $count - 1) {
if (static::isValidRange($i, SlotRange::MAX_SLOTS)) {
$gaps[] = new NullSlotRange($i, SlotRange::MAX_SLOTS);
}
}
}
return $gaps;
}
/**
* Sort slot ranges by start index.
*
* @param SlotRange[] $slotRanges
*
* @return void
*/
protected function sortSlotRanges(array &$slotRanges)
{
usort(
$slotRanges,
function (SlotRange $a, SlotRange $b) {
if ($a->getStart() == $b->getStart()) {
return 0;
}
return $a->getStart() < $b->getStart() ? -1 : 1;
}
);
}
/**
* Compact adjacent slot ranges with the same connection.
*
* @param SlotRange[] $slotRanges
*
* @return SlotRange[]
*/
protected function compactSlotRanges(array $slotRanges)
{
if (empty($slotRanges)) {
return [];
}
$compacted = [];
$count = count($slotRanges);
$i = 0;
$carry = $slotRanges[0];
while ($i < $count) {
$next = $slotRanges[$i + 1] ?? null;
if (
!is_null($next)
&& ($carry->getEnd() + 1) === $next->getStart()
&& $carry->getConnection() === $next->getConnection()
) {
$carry = new SlotRange($carry->getStart(), $next->getEnd(), $carry->getConnection());
} else {
$compacted[] = $carry;
$carry = $next;
}
$i++;
}
return array_values($compacted);
}
}
+145
View File
@@ -0,0 +1,145 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster;
use Countable;
use OutOfBoundsException;
/**
* Represents a range of slots in a Redis cluster.
*/
class SlotRange implements Countable
{
/**
* Maximum number of slots in a Redis cluster is 16384.
*/
public const MAX_SLOTS = 0x3FFF;
/**
* Starting slot of the range.
*
* @var int
*/
protected $start;
/**
* Ending slot of the range.
*
* @var int
*/
protected $end;
/**
* Connection to the server hosting this slot range.
*
* @var string
*/
protected $connection;
public function __construct(int $start, int $end, string $connection)
{
if (!static::isValidRange($start, $end)) {
throw new OutOfBoundsException("Invalid slot range $start-$end for `$connection`");
}
$this->start = $start;
$this->end = $end;
$this->connection = $connection;
}
/**
* Checks if a slot range is valid.
*
* @param int $first
* @param int $last
*
* @return bool
*/
public static function isValidRange($first, $last)
{
return $first >= 0 && $first <= self::MAX_SLOTS && $last >= 0x0000 && $last <= self::MAX_SLOTS && $first <= $last;
}
/**
* Returns the start slot index of this range.
*
* @return int
*/
public function getStart()
{
return $this->start;
}
/**
* Returns the end slot index of this range.
*
* @return int
*/
public function getEnd()
{
return $this->end;
}
/**
* Returns the connection to the server hosting this slot range.
*
* @return string
*/
public function getConnection()
{
return $this->connection;
}
/**
* Checks if the specific slot is contained in this range.
*
* @param int $slot
*
* @return bool
*/
public function hasSlot(int $slot)
{
return $this->start <= $slot && $this->end >= $slot;
}
/**
* Returns an array of connection strings for each slot in this range.
*
* @return string[]
*/
public function toArray(): array
{
return array_fill($this->start, $this->end - $this->start + 1, $this->connection);
}
/**
* Returns the number of slots in this range.
*
* @return int
*/
public function count(): int
{
return $this->end - $this->start + 1;
}
/**
* Checks if this range has an intersection with the given slot range.
*
* @param SlotRange $slotRange
*
* @return bool
*/
public function hasIntersectionWith(SlotRange $slotRange): bool
{
return $this->start <= $slotRange->getEnd() && $this->end >= $slotRange->getStart();
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
class BITFIELD_RO extends RedisCommand
{
/**
* @return string
*/
public function getId()
{
return 'BITFIELD_RO';
}
/**
* @param array $arguments
* @return void
*/
public function setArguments(array $arguments)
{
$processedArguments = [$arguments[0]];
if (array_key_exists(1, $arguments) && is_array($arguments[1])) {
// Convert encoding => offset, into GET, encoding, offset
array_walk($arguments[1], function ($value, $key) use (&$processedArguments) {
array_push($processedArguments, 'GET', $key, $value);
});
}
parent::setArguments($processedArguments);
}
}
+1
View File
@@ -15,6 +15,7 @@ namespace Predis\Command\Redis\Container;
use Predis\Response\Status;
/**
* @method array cat(string $category = null)
* @method Status dryRun(string $username, string $command, ...$arguments)
* @method array getUser(string $username)
* @method Status setUser(string $username, string ...$rules)
+35
View File
@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
class HGETDEL extends RedisCommand
{
public function getId()
{
return 'HGETDEL';
}
/**
* @param array $arguments
* @return void
*/
public function setArguments(array $arguments)
{
$processedArguments = [$arguments[0], 'FIELDS', count($arguments[1])];
$processedArguments = array_merge($processedArguments, $arguments[1]);
parent::setArguments($processedArguments);
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
use UnexpectedValueException;
class HGETEX extends RedisCommand
{
public const NULL = '';
public const EX = 'ex';
public const PX = 'px';
public const EXAT = 'exat';
public const PXAT = 'pxat';
public const PERSIST = 'persist';
/**
* @var string[]
*/
private static $modifierEnum = [
self::EX => 'EX',
self::PX => 'PX',
self::EXAT => 'EXAT',
self::PXAT => 'PXAT',
self::PERSIST => 'PERSIST',
];
public function getId()
{
return 'HGETEX';
}
public function setArguments(array $arguments)
{
$processedArguments = [$arguments[0]];
// Only required arguments
if (!array_key_exists(2, $arguments) || $arguments[2] == '') {
array_push($processedArguments, 'FIELDS', count($arguments[1]));
$processedArguments = array_merge($processedArguments, $arguments[1]);
parent::setArguments($processedArguments);
return;
}
if (!in_array(strtoupper($arguments[2]), self::$modifierEnum)) {
$enumValues = implode(', ', array_keys(self::$modifierEnum));
throw new UnexpectedValueException("Modifier argument accepts only: {$enumValues} values");
}
// PERSIST requires no additional value
if (strtoupper($arguments[2]) === self::$modifierEnum['persist']) {
$processedArguments[] = self::$modifierEnum['persist'];
array_push($processedArguments, 'FIELDS', count($arguments[1]));
$processedArguments = array_merge($processedArguments, $arguments[1]);
parent::setArguments($processedArguments);
return;
}
if (!array_key_exists(3, $arguments) || !is_int($arguments[3])) {
throw new UnexpectedValueException('Modifier value is missing or incorrect type');
}
// Order matters so FIELDS should be at the end
array_push($processedArguments, self::$modifierEnum[strtolower($arguments[2])], $arguments[3], 'FIELDS', count($arguments[1]));
$processedArguments = array_merge($processedArguments, $arguments[1]);
parent::setArguments($processedArguments);
}
}
+117
View File
@@ -0,0 +1,117 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
use UnexpectedValueException;
class HSETEX extends RedisCommand
{
public const TTL_NULL = '';
public const TTL_EX = 'ex';
public const TTL_PX = 'px';
public const TTL_EXAT = 'exat';
public const TTL_PXAT = 'pxat';
public const TTL_KEEP_TTL = 'keepttl';
public const SET_NULL = '';
public const SET_FNX = 'fnx';
public const SET_FXX = 'fxx';
/**
* @var string[]
*/
private static $ttlModifierEnum = [
self::TTL_EX => 'EX',
self::TTL_PX => 'PX',
self::TTL_EXAT => 'EXAT',
self::TTL_PXAT => 'PXAT',
self::TTL_KEEP_TTL => 'KEEPTTL',
];
/**
* @var string[]
*/
private static $setModifierEnum = [
self::SET_FNX => 'FNX',
self::SET_FXX => 'FXX',
];
public function getId()
{
return 'HSETEX';
}
public function setArguments(array $arguments)
{
$processedArguments = [$arguments[0]];
$flatArray = [];
// Convert key => value, into key, value
array_walk($arguments[1], function ($value, $key) use (&$flatArray) {
array_push($flatArray, $key, $value);
});
// Only required arguments
if (!array_key_exists(2, $arguments)) {
array_push($processedArguments, 'FIELDS', count($flatArray) / 2);
$processedArguments = array_merge($processedArguments, $flatArray);
parent::setArguments($processedArguments);
return;
}
if ($arguments[2] !== '') {
if (!in_array(strtoupper($arguments[2]), self::$setModifierEnum)) {
$enumValues = implode(', ', array_keys(self::$setModifierEnum));
throw new UnexpectedValueException("Modifier argument accepts only: {$enumValues} values");
}
$processedArguments[] = self::$setModifierEnum[strtolower($arguments[2])];
}
// Required + set modifier
if (!array_key_exists(3, $arguments) || $arguments[3] == '') {
array_push($processedArguments, 'FIELDS', count($flatArray) / 2);
$processedArguments = array_merge($processedArguments, $flatArray);
parent::setArguments($processedArguments);
return;
}
if (!in_array(strtoupper($arguments[3]), self::$ttlModifierEnum)) {
$enumValues = implode(', ', array_keys(self::$ttlModifierEnum));
throw new UnexpectedValueException("Modifier argument accepts only: {$enumValues} values");
}
// KEEPTTL requires no additional value
if (strtoupper($arguments[3]) === self::$ttlModifierEnum[self::TTL_KEEP_TTL]) {
$processedArguments[] = self::$ttlModifierEnum[self::TTL_KEEP_TTL];
array_push($processedArguments, 'FIELDS', count($flatArray) / 2);
$processedArguments = array_merge($processedArguments, $flatArray);
parent::setArguments($processedArguments);
return;
}
if (!array_key_exists(4, $arguments) || !is_int($arguments[4])) {
throw new UnexpectedValueException('Modifier value is missing or incorrect type');
}
// Order matters so FIELDS should be at the end
array_push($processedArguments, self::$ttlModifierEnum[strtolower($arguments[3])], $arguments[4], 'FIELDS', count($flatArray) / 2);
$processedArguments = array_merge($processedArguments, $flatArray);
parent::setArguments($processedArguments);
}
}
+1 -7
View File
@@ -30,13 +30,7 @@ class FTAGGREGATE extends RedisCommand
public function setArguments(array $arguments)
{
[$index, $query] = $arguments;
if (!empty($arguments[2]) && !in_array("DIALECT", $arguments[2]->toArray())) {
// Default dialect is 2
$arguments[2]->dialect(2);
}
$commandArguments = (!empty($arguments[2])) ? $arguments[2]->toArray() : ['DIALECT', 2];
$commandArguments = (!empty($arguments[2])) ? $arguments[2]->toArray() : [];
parent::setArguments(array_merge(
[$index, $query],
+4
View File
@@ -13,8 +13,12 @@
namespace Predis\Command\Redis\Search;
use Predis\Command\Command as RedisCommand;
use Predis\Command\Redis\CONFIG;
/**
* @deprecated FT.CONFIG GET and SET is deprecated since Redis 8.0.
* @see CONFIG if you want to manipulate search configuration
*
* @see https://redis.io/commands/ft.config-get/
* @see https://redis.io/commands/ft.config-set/
*
+1 -7
View File
@@ -29,13 +29,7 @@ class FTEXPLAIN extends RedisCommand
public function setArguments(array $arguments)
{
[$index, $query] = $arguments;
if (!empty($arguments[2]) && !in_array("DIALECT", $arguments[2]->toArray())) {
// Default dialect is 2
$arguments[2]->dialect(2);
}
$commandArguments = ['DIALECT', 2];
$commandArguments = [];
if (!empty($arguments[2])) {
$commandArguments = $arguments[2]->toArray();
+1 -7
View File
@@ -29,13 +29,7 @@ class FTSEARCH extends RedisCommand
public function setArguments(array $arguments)
{
[$index, $query] = $arguments;
if (!empty($arguments[2]) && !in_array("DIALECT", $arguments[2]->toArray())) {
// Default dialect is 2
$arguments[2]->dialect(2);
}
$commandArguments = (!empty($arguments[2])) ? $arguments[2]->toArray() : ['DIALECT', 2];
$commandArguments = (!empty($arguments[2])) ? $arguments[2]->toArray() : [];
parent::setArguments(array_merge(
[$index, $query],
+1 -7
View File
@@ -24,13 +24,7 @@ class FTSPELLCHECK extends RedisCommand
public function setArguments(array $arguments)
{
[$index, $query] = $arguments;
if (!empty($arguments[2]) && !in_array("DIALECT", $arguments[2]->toArray())) {
// Default dialect is 2
$arguments[2]->dialect(2);
}
$commandArguments = ['DIALECT', 2];
$commandArguments = [];
if (!empty($arguments[2])) {
$commandArguments = $arguments[2]->toArray();
+26
View File
@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis\Search;
use Predis\Command\Command as RedisCommand;
class FT_LIST extends RedisCommand
{
/**
* @return string
*/
public function getId()
{
return 'FT._LIST';
}
}
+12 -3
View File
@@ -254,7 +254,7 @@ class RedisCluster implements ClusterInterface, IteratorAggregate, Countable
}
if (!$connection = $this->getRandomConnection()) {
throw new ClientException('No connections left in the pool for `CLUSTER SLOTS`');
throw new ClientException('No connections left in the pool for `CLUSTER SLOTS` (' . $exception->getMessage() . ')');
}
usleep($retryAfter * 1000);
@@ -337,10 +337,19 @@ class RedisCluster implements ClusterInterface, IteratorAggregate, Countable
{
$separator = strrpos($connectionID, ':');
return $this->connections->create([
$parameters = [
'host' => substr($connectionID, 0, $separator),
'port' => substr($connectionID, $separator + 1),
]);
];
$existConnection = current($this->pool);
if ($existConnection instanceof NodeConnectionInterface) {
$existParameters = $existConnection->getParameters()->toArray();
unset($existParameters['alias'], $existParameters['slots']);
$parameters = array_merge($existParameters, $parameters);
}
return $this->connections->create($parameters);
}
/**
+5
View File
@@ -165,6 +165,11 @@ class Parameters implements ParametersInterface
}
}
public function __set($parameter, $value)
{
$this->parameters[$parameter] = $value;
}
/**
* {@inheritdoc}
*/
+1
View File
@@ -27,6 +27,7 @@ namespace Predis\Connection;
* @property float $timeout Timeout for the connect() operation.
* @property float $read_write_timeout Timeout for read() and write() operations.
* @property bool $persistent Leaves the connection open after a GC collection.
* @property string $conn_uid Unique identifier of connection, needs to create a multiple persistent connections to the same resource.
* @property string $password Password to access Redis (see the AUTH command).
* @property string $database Database index (see the SELECT command).
* @property bool $async_connect Performs the connect() operation asynchronously.
+21
View File
@@ -35,6 +35,15 @@ use Predis\Response\Status as StatusResponse;
*/
class StreamConnection extends AbstractConnection
{
/**
* @param ParametersInterface $parameters
*/
public function __construct(ParametersInterface $parameters)
{
parent::__construct($parameters);
$this->parameters->conn_uid = spl_object_hash($this);
}
/**
* Disconnects from the server and destroys the underlying resource when the
* garbage collector kicks in only if the connection has not been marked as
@@ -105,6 +114,18 @@ class StreamConnection extends AbstractConnection
$timeout = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0);
$context = stream_context_create(['socket' => ['tcp_nodelay' => (bool) $parameters->tcp_nodelay]]);
if (
(isset($parameters->persistent) && $parameters->persistent)
&& (isset($parameters->conn_uid) && $parameters->conn_uid)
) {
$conn_uid = '/' . $parameters->conn_uid;
} else {
$conn_uid = '';
}
// Needs to create multiple persistent connections to the same resource
$address = $address . $conn_uid;
if (!$resource = @stream_socket_client($address, $errno, $errstr, $timeout, $flags, $context)) {
$this->onConnectionError(trim($errstr), $errno);
}
+1 -1
View File
@@ -175,7 +175,7 @@ abstract class PredisTestCase extends PHPUnit\Framework\TestCase
return [
'scheme' => 'tcp',
'host' => constant('REDIS_SERVER_HOST'),
'port' => ($this->isStackTest()) ? constant('REDIS_STACK_SERVER_PORT') : constant('REDIS_SERVER_PORT'),
'port' => ($this->isStackTest()) ? getenv('REDIS_STACK_SERVER_PORT') : constant('REDIS_SERVER_PORT'),
'database' => constant('REDIS_SERVER_DBNUM'),
];
}
+38
View File
@@ -1283,6 +1283,44 @@ class ClientTest extends PredisTestCase
$this->assertSame(Client::VERSION, $libVer);
}
/**
* @group connected
*/
public function testClientsCreateDifferentPersistentConnections(): void
{
$client1 = new Client($this->getParameters(['database' => 14, 'persistent' => true]));
$client2 = new Client($this->getParameters(['database' => 15, 'persistent' => true]));
$client1->set('foo', 'bar');
$client2->set('foo', 'baz');
$this->assertSame('bar', $client1->get('foo'));
$this->assertSame('baz', $client2->get('foo'));
}
/**
* @group connected
* @group cluster
* @requiresRedisVersion >= 2.0.0
*/
public function testClusterClientsCreateDifferentPersistentConnections(): void
{
$client1 = new Client(
$this->getDefaultParametersArray(),
['cluster' => 'redis', 'parameters' => ['persistent' => true]]
);
$client2 = new Client(
$this->getDefaultParametersArray(),
['cluster' => 'redis', 'parameters' => ['persistent' => true]]
);
$client1->set('{shard1}foo', 'bar');
$client2->set('{shard2}foo', 'baz');
$this->assertSame('bar', $client1->get('{shard1}foo'));
$this->assertSame('baz', $client2->get('{shard2}foo'));
}
// ******************************************************************** //
// ---- HELPER METHODS ------------------------------------------------ //
// ******************************************************************** //
+473
View File
@@ -0,0 +1,473 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster;
use Predis\Cluster\SimpleSlotMap as SlotMap;
use PredisTestCase;
class SimpleSlotMapTest extends PredisTestCase
{
/**
* @group disconnected
*/
public function testIsValidReturnsTrueOnValidSlot(): void
{
$this->assertTrue(SlotMap::isValid(0));
$this->assertTrue(SlotMap::isValid(16383));
$this->assertTrue(SlotMap::isValid(5000));
$this->assertTrue(SlotMap::isValid('5000'));
}
/**
* @group disconnected
*/
public function testIsValidReturnsFalseOnInvalidSlot(): void
{
$this->assertFalse(SlotMap::isValid(-1));
$this->assertFalse(SlotMap::isValid(16384));
}
/**
* @group disconnected
*/
public function testIsValidRangeReturnsTrueOnValidSlotRange(): void
{
$this->assertTrue(SlotMap::isValidRange(0, 16383));
$this->assertTrue(SlotMap::isValidRange(2000, 2999));
$this->assertTrue(SlotMap::isValidRange(3000, 3000));
}
/**
* @group disconnected
*/
public function testIsValidRangeReturnsFalseOnInvalidSlotRange(): void
{
$this->assertFalse(SlotMap::isValidRange(0, 16384));
$this->assertFalse(SlotMap::isValidRange(-1, 16383));
$this->assertFalse(SlotMap::isValidRange(-1, 16384));
$this->assertFalse(SlotMap::isValidRange(2999, 2000));
}
/**
* @group disconnected
*/
public function testToArrayReturnsEmptyArrayOnEmptySlotMap(): void
{
$slotmap = new SlotMap();
$this->assertEmpty($slotmap->toArray());
}
/**
* @group disconnected
*/
public function testSetSlotsAssignsSpecifiedNodeToSlotRange(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(0, 5460, '127.0.0.1:6379');
$slotmap->setSlots(5461, 10922, '127.0.0.1:6380');
$slotmap->setSlots(10923, 16383, '127.0.0.1:6381');
$expectedMap = array_merge(
array_fill(0, 5461, '127.0.0.1:6379'),
array_fill(5461, 5462, '127.0.0.1:6380'),
array_fill(10923, 5461, '127.0.0.1:6381')
);
$this->assertSame($expectedMap, $slotmap->toArray());
}
/**
* @group disconnected
*/
public function testSetSlotsOverwritesSlotRange(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(0, 5460, '127.0.0.1:6379');
$slotmap->setSlots(1000, 2000, '127.0.0.1:6380');
$expectedMap =
array_fill(0, 5461, '127.0.0.1:6379') +
array_fill(1000, 2000, '127.0.0.1:6380');
$this->assertSame($expectedMap, $slotmap->toArray());
}
/**
* @group disconnected
*/
public function testSetSlotsAssignsSingleSlotWhenFirstAndLastSlotMatch(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(10, 10, '127.0.0.1:6379');
$this->assertSame([10 => '127.0.0.1:6379'], $slotmap->toArray());
}
/**
* @group disconnected
*/
public function testSetSlotsCastsValueToString(): void
{
$slotmap = new SlotMap();
$connection = $this->getMockConnection();
$connection
->expects($this->once())
->method('__toString')
->willReturn('127.0.0.1:6379');
$slotmap->setSlots(10, 10, $connection);
$this->assertSame([10 => '127.0.0.1:6379'], $slotmap->toArray());
}
/**
* @group disconnected
*/
public function testSetSlotsThrowsExceptionOnInvalidSlotRange(): void
{
$this->expectException('OutOfBoundsException');
$this->expectExceptionMessage('Invalid slot range 0-16384 for `127.0.0.1:6379`');
$slotmap = new SlotMap();
$slotmap->setSlots(0, 16384, '127.0.0.1:6379');
}
/**
* @group disconnected
*/
public function testGetSlotsReturnsEmptyArrayOnEmptySlotMap(): void
{
$slotmap = new SlotMap();
$this->assertEmpty($slotmap->getSlots(3, 11));
}
/**
* @group disconnected
*/
public function testGetSlotsReturnsDictionaryOfSlotsWithAssignedNodes(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(0, 5, '127.0.0.1:6379');
$slotmap->setSlots(10, 13, '127.0.0.1:6380');
$expectedMap = [
3 => '127.0.0.1:6379',
4 => '127.0.0.1:6379',
5 => '127.0.0.1:6379',
10 => '127.0.0.1:6380',
11 => '127.0.0.1:6380',
];
$this->assertSame($expectedMap, $slotmap->getSlots(3, 11));
}
/**
* @group disconnected
*/
public function testGetSlotsReturnsEmptyArrayOnEmptySlotRange(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(0, 5, '127.0.0.1:6379');
$slotmap->setSlots(10, 13, '127.0.0.1:6380');
$this->assertEmpty($slotmap->getSlots(100, 200));
}
/**
* @group disconnected
*/
public function testGetSlotsThrowsExceptionOnInvalidSlotRange(): void
{
$this->expectException('OutOfBoundsException');
$this->expectExceptionMessage('Invalid slot range 0-16384');
$slotmap = new SlotMap();
$slotmap->getSlots(0, 16384);
}
/**
* @group disconnected
*/
public function testIsEmptyReturnsTrueOnEmptySlotMap(): void
{
$slotmap = new SlotMap();
$this->assertTrue($slotmap->isEmpty());
}
/**
* @group disconnected
*/
public function testIsEmptyReturnsFalseOnNonEmptySlotMap(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(0, 5460, '127.0.0.1:6379');
$this->assertFalse($slotmap->isEmpty());
}
/**
* @group disconnected
*/
public function testCountReturnsZeroOnEmptySlotMap(): void
{
$slotmap = new SlotMap();
$this->assertCount(0, $slotmap);
}
/**
* @group disconnected
*/
public function testCountReturnsAssignedSlotsInSlotMap(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(0, 5460, '127.0.0.1:6379');
$this->assertCount(5461, $slotmap);
$slotmap->setSlots(5461, 10922, '127.0.0.1:6380');
$this->assertCount(10923, $slotmap);
$slotmap->setSlots(10923, 16383, '127.0.0.1:6381');
$this->assertCount(16384, $slotmap);
}
/**
* @group disconnected
*/
public function testResetEmptiesSlotMap(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(0, 5460, '127.0.0.1:6379');
$slotmap->setSlots(5461, 10922, '127.0.0.1:6380');
$slotmap->setSlots(10923, 16383, '127.0.0.1:6381');
$this->assertFalse($slotmap->isEmpty());
$slotmap->reset();
$this->assertTrue($slotmap->isEmpty());
}
/**
* @group disconnected
*/
public function testGetNodesReturnsEmptyArrayOnEmptySlotMap(): void
{
$slotmap = new SlotMap();
$this->assertEmpty($slotmap->getNodes());
}
/**
* @group disconnected
*/
public function testGetNodesReturnsArrayOfNodesInSlotMap(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(0, 5460, '127.0.0.1:6379');
$slotmap->setSlots(5461, 10922, '127.0.0.1:6380');
$slotmap->setSlots(10923, 16383, '127.0.0.1:6381');
$this->assertSame(['127.0.0.1:6379', '127.0.0.1:6380', '127.0.0.1:6381'], $slotmap->getNodes());
}
/**
* @group disconnected
*/
public function testOffsetExistsReturnsTrueOnAssignedSlot(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(0, 5460, '127.0.0.1:6379');
$this->assertTrue(isset($slotmap[0]));
$this->assertTrue(isset($slotmap[2000]));
}
/**
* @group disconnected
*/
public function testOffsetExistsReturnsFalseOnAssignedSlot(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(0, 5460, '127.0.0.1:6379');
$this->assertFalse(isset($slotmap[6000]));
}
/**
* @group disconnected
*/
public function testOffsetExistsReturnsFalseOnInvalidSlot(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(0, 5460, '127.0.0.1:6379');
$this->assertFalse(isset($slotmap[-100]));
$this->assertFalse(isset($slotmap[16384]));
}
/**
* @group disconnected
*/
public function testOffsetGetReturnsNodeOfAssignedSlot(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(0, 5460, '127.0.0.1:6379');
$slotmap->setSlots(5461, 10922, '127.0.0.1:6380');
$slotmap->setSlots(10923, 16383, '127.0.0.1:6381');
$this->assertSame('127.0.0.1:6379', $slotmap[0]);
$this->assertSame('127.0.0.1:6380', $slotmap[5461]);
$this->assertSame('127.0.0.1:6381', $slotmap[10923]);
}
/**
* @group disconnected
*/
public function testOffsetGetReturnsNullOnUnassignedSlot(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(0, 5460, '127.0.0.1:6379');
$this->assertNull($slotmap[5461]);
}
/**
* @group disconnected
*/
public function testOffsetGetReturnsNullOnInvalidSlot(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(0, 5460, '127.0.0.1:6379');
$this->assertNull($slotmap[-100]);
$this->assertNull($slotmap[16384]);
}
/**
* @group disconnected
*/
public function testOffsetUnsetRemovesSlotAssignment(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(0, 5460, '127.0.0.1:6379');
$this->assertTrue(isset($slotmap[100]));
unset($slotmap[100]);
$this->assertFalse(isset($slotmap[100]));
}
/**
* @group disconnected
*/
public function testOffsetUnsetDoesNotDoAnythingOnUnassignedSlot(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(0, 5460, '127.0.0.1:6379');
$this->assertFalse(isset($slotmap[5461]));
unset($slotmap[5461]);
$this->assertFalse(isset($slotmap[5461]));
}
/**
* @group disconnected
*/
public function testOffsetSetAssignsNodeToSlot(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(0, 5460, '127.0.0.1:6379');
$this->assertSame('127.0.0.1:6380', $slotmap[100] = '127.0.0.1:6380');
$this->assertSame('127.0.0.1:6380', $slotmap[100]);
$this->assertNull($slotmap[5461]);
$this->assertSame('127.0.0.1:6380', $slotmap[5461] = '127.0.0.1:6380');
$this->assertSame('127.0.0.1:6380', $slotmap[5461]);
}
/**
* @group disconnected
*/
public function testOffsetSetCastsValueToString(): void
{
$slotmap = new SlotMap();
$connection = $this->getMockConnection();
$connection
->expects($this->once())
->method('__toString')
->willReturn('127.0.0.1:6379');
$this->assertSame($connection, $slotmap[0] = $connection);
$this->assertSame('127.0.0.1:6379', $slotmap[0]);
}
/**
* @group disconnected
*/
public function testOffsetSetThrowsExceptionOnInvalidSlot(): void
{
$this->expectException('OutOfBoundsException');
$this->expectExceptionMessage('Invalid slot 16384 for `127.0.0.1:6379`');
$slotmap = new SlotMap();
$slotmap[16384] = '127.0.0.1:6379';
}
/**
* @group disconnected
*/
public function testGetIteratorReturnsIteratorOverSlotMap(): void
{
$slotmap = new SlotMap();
$slotmap->setSlots(0, 5460, '127.0.0.1:6379');
$slotmap->setSlots(5461, 10922, '127.0.0.1:6380');
$slotmap->setSlots(10923, 16383, '127.0.0.1:6381');
$expectedMap = array_merge(
array_fill(0, 5461, '127.0.0.1:6379'),
array_fill(5461, 5462, '127.0.0.1:6380'),
array_fill(10923, 5461, '127.0.0.1:6381')
);
$this->assertSame($expectedMap, iterator_to_array($slotmap));
}
}
+134
View File
@@ -0,0 +1,134 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Cluster;
use PredisTestCase;
class SlotRangeTest extends PredisTestCase
{
/**
* @group disconnected
*/
public function testMaxSlotsEquals16383()
{
$this->assertEquals(SlotRange::MAX_SLOTS, 16383);
}
/**
* @group disconnected
*/
public function testConstructorThrowExceptionOnInvalidSlotRange(): void
{
$this->expectException('OutOfBoundsException');
$this->expectExceptionMessage('Invalid slot range 600-300 for `c1`');
new SlotRange(600, 300, 'c1');
}
/**
* @group disconnected
*/
public function testIsValidRangeReturnsTrueOnValidSlotRange()
{
$this->assertTrue(SlotRange::isValidRange(0, SlotRange::MAX_SLOTS));
$this->assertTrue(SlotRange::isValidRange(2000, 2999));
$this->assertTrue(SlotRange::isValidRange(3000, 3000));
}
/**
* @group disconnected
*/
public function testIsValidRangeReturnsFalseOnInvalidSlotRange()
{
$this->assertFalse(SlotRange::isValidRange(0, 16384));
$this->assertFalse(SlotRange::isValidRange(-1, 16383));
$this->assertFalse(SlotRange::isValidRange(-1, 16384));
$this->assertFalse(SlotRange::isValidRange(2999, 2000));
}
/**
* @group disconnected
*/
public function testPropertySetters()
{
$range = new SlotRange(2000, 5000, 'c1');
$this->assertEquals(2000, $range->getStart());
$this->assertEquals(5000, $range->getEnd());
$this->assertEquals('c1', $range->getConnection());
}
/**
* @group disconnected
*/
public function testHasSlot()
{
$range = new SlotRange(2000, 4000, 'c1');
$this->assertTrue($range->hasSlot(2000));
$this->assertTrue($range->hasSlot(4000));
$this->assertTrue($range->hasSlot(3000));
$this->assertFalse($range->hasSlot(1000));
$this->assertFalse($range->hasSlot(5000));
}
/**
* @group disconnected
*/
public function testToArray()
{
$range = new SlotRange(2000, 2005, 'c1');
$array = $range->toArray();
$this->assertSame([
2000 => 'c1',
2001 => 'c1',
2002 => 'c1',
2003 => 'c1',
2004 => 'c1',
2005 => 'c1',
], $array);
}
/**
* @group disconnected
*/
public function testCount()
{
$range = new SlotRange(2000, 3000, 'c1');
$this->assertEquals(1001, $range->count());
$this->assertEquals(1001, count($range));
}
/**
* @group disconnected
*/
public function testHasIntersectionWith()
{
$original = new SlotRange(2000, 3000, 'c1');
$this->assertFalse($original->hasIntersectionWith(new SlotRange(1000, 1500, 'c1')));
$this->assertFalse($original->hasIntersectionWith(new SlotRange(3001, 5000, 'c1')));
$this->assertFalse($original->hasIntersectionWith(new SlotRange(1999, 1999, 'c1')));
$this->assertFalse($original->hasIntersectionWith(new SlotRange(3001, 3001, 'c1')));
$this->assertTrue($original->hasIntersectionWith(new SlotRange(1500, 6000, 'c1')));
$this->assertTrue($original->hasIntersectionWith(new SlotRange(2500, 2999, 'c1')));
$this->assertTrue($original->hasIntersectionWith(new SlotRange(2000, 2999, 'c1')));
$this->assertTrue($original->hasIntersectionWith(new SlotRange(2500, 3000, 'c1')));
$this->assertTrue($original->hasIntersectionWith(new SlotRange(1500, 2000, 'c1')));
$this->assertTrue($original->hasIntersectionWith(new SlotRange(3000, 3500, 'c1')));
$this->assertTrue($original->hasIntersectionWith(new SlotRange(2000, 2000, 'c1')));
$this->assertTrue($original->hasIntersectionWith(new SlotRange(3000, 3000, 'c1')));
}
}
@@ -1008,6 +1008,14 @@ class KeyPrefixProcessorTest extends PredisTestCase
['key', 'MAXLEN', 100],
['prefix:key', 'MAXLEN', 100],
],
['ZPOPMIN',
['key'],
['prefix:key'],
],
['ZPOPMAX',
['key'],
['prefix:key'],
],
/* ---------------- Redis 6.2 ---------------- */
['GETDEL',
['key'],
+169
View File
@@ -12,6 +12,7 @@
namespace Predis\Command\Redis;
use Predis\Command\Argument\Search\SchemaFields\TextField;
use Predis\Response\ServerException;
class ACL_Test extends PredisCommandTestCase
@@ -132,6 +133,174 @@ class ACL_Test extends PredisCommandTestCase
}
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testModuleCategoriesAppearsInListOfAllCategories(): void
{
$redis = $this->getClient();
$allCategories = $redis->acl->cat();
foreach (['bloom', 'cuckoo', 'cms', 'topk', 'tdigest', 'search', 'timeseries', 'json'] as $category) {
$this->assertContains($category, $allCategories);
}
}
/**
* @group connected
* @group relay-incompatible
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testSetModuleCommandPrivileges(): void
{
$redis = $this->getClient();
$this->assertEquals(
'OK',
$redis->acl->setUser(
'testUser',
'reset',
'nopass',
'on'
)
);
$this->assertEquals('OK', $redis->auth('testUser', ''));
$this->expectException(ServerException::class);
$redis->ftcreate('test', [new TextField('foo')]);
$this->assertEquals(
'OK',
$redis->acl->setUser(
'testUser',
'+ft.create',
'+ft.search'
)
);
$this->assertEquals('OK', $redis->ftcreate('test', [new TextField('foo')]));
$this->assertEmpty($redis->ftsearch('test', '*'));
$this->expectException(ServerException::class);
$redis->jsonset('test', '$', '{"key":"value"}');
$this->assertEquals(
'OK',
$redis->acl->setUser(
'testUser',
'+json.set'
)
);
$this->assertEquals('OK', $redis->jsonset('test', '$', '{"key":"value"}'));
$this->expectException(ServerException::class);
$redis->bfadd('test', 'value');
$this->assertEquals(
'OK',
$redis->acl->setUser(
'testUser',
'+bf.add'
)
);
$this->assertEquals(1, $redis->bfadd('test', 'value'));
$this->expectException(ServerException::class);
$redis->tsadd('test', time(), 0.01);
$this->assertEquals(
'OK',
$redis->acl->setUser(
'testUser',
'+ts.add'
)
);
$this->assertEquals(1, $redis->tsadd('test', time(), 0.01));
}
/**
* @group connected
* @group relay-incompatible
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testSetModuleCategoryPrivileges(): void
{
$redis = $this->getClient();
$this->assertEquals(
'OK',
$redis->acl->setUser(
'testUser',
'reset',
'nopass',
'on'
)
);
$this->assertEquals('OK', $redis->auth('testUser', ''));
$this->expectException(ServerException::class);
$redis->ftcreate('test', [new TextField('foo')]);
$this->assertEquals(
'OK',
$redis->acl->setUser(
'testUser',
'+@search'
)
);
$this->assertEquals('OK', $redis->ftcreate('test', [new TextField('foo')]));
$this->assertEmpty($redis->ftsearch('test', '*'));
$this->expectException(ServerException::class);
$redis->jsonset('test', '$', '{"key":"value"}');
$this->assertEquals(
'OK',
$redis->acl->setUser(
'testUser',
'+@json'
)
);
$this->assertEquals('OK', $redis->jsonset('test', '$', '{"key":"value"}'));
$this->expectException(ServerException::class);
$redis->bfadd('test', 'value');
$this->assertEquals(
'OK',
$redis->acl->setUser(
'testUser',
'+@bloom'
)
);
$this->assertEquals(1, $redis->bfadd('test', 'value'));
$this->expectException(ServerException::class);
$redis->tsadd('test', time(), 0.01);
$this->assertEquals(
'OK',
$redis->acl->setUser(
'testUser',
'+@timeseries'
)
);
$this->assertEquals(1, $redis->tsadd('test', time(), 0.01));
}
/**
* @group connected
* @return void
@@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
class BITFIELD_RO_Test extends PredisCommandTestCase
{
/**
* {@inheritdoc}
*/
protected function getExpectedCommand(): string
{
return BITFIELD_RO::class;
}
/**
* {@inheritdoc}
*/
protected function getExpectedId(): string
{
return 'BITFIELD_RO';
}
/**
* @group disconnected
* @dataProvider argumentsProvider
*/
public function testFilterArguments(array $actualArguments, array $expectedArguments): void
{
$command = $this->getCommand();
$command->setArguments($actualArguments);
$this->assertSame($expectedArguments, $command->getArguments());
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 6.2.0
*/
public function testReturnBitsOfSpecificString()
{
$redis = $this->getClient();
$redis->set('foo', 'bar');
$this->assertSame([6, 98], $redis->bitfield_ro('foo', ['u4' => 0, 'i8' => 0]));
}
public function argumentsProvider(): array
{
return [
'with default arguments' => [
['key'],
['key'],
],
'with single encoding-offset entry' => [
['key', ['encoding' => 'offset']],
['key', 'GET', 'encoding', 'offset'],
],
'with multiple encoding-offset entry' => [
['key', ['encoding' => 'offset', 'encoding1' => 'offset1', 'encoding2' => 'offset2']],
['key', 'GET', 'encoding', 'offset', 'GET', 'encoding1', 'offset1', 'GET', 'encoding2', 'offset2'],
],
];
}
}
@@ -12,6 +12,8 @@
namespace Predis\Command\Redis;
use Predis\Response\ServerException;
/**
* @group commands
* @group realm-server
@@ -137,6 +139,48 @@ class CONFIG_Test extends PredisCommandTestCase
$redis->config('SET', 'loglevel', $previous['loglevel']);
}
/**
* @group connected
* @group relay-incompatible
* @requiresRedisVersion >= 7.9.0
*/
public function testOverrideDefaultDialectWithConfigCommand()
{
$redis = $this->getClient();
$default_dialect = (int) $redis
->config('GET', 'search-default-dialect')['search-default-dialect'];
$this->assertEquals('OK', $redis->config('SET', 'search-default-dialect', 2));
$this->assertEquals(2, (int) $redis->ftconfig->get('DEFAULT_DIALECT')[0][1]);
$this->assertEquals(2,
(int) $redis->config('GET', 'search-default-dialect')['search-default-dialect']);
$this->assertEquals(
'OK',
$redis->config('SET', 'search-default-dialect', $default_dialect)
);
}
/**
* @group connected
* @requiresRedisVersion >= 7.9.0
*/
public function testSetGetSearchConfiguration()
{
$redis = $this->getClient();
$this->assertGreaterThan(0,
(int) $redis->config('GET', 'search-timeout')['search-timeout']);
$this->assertGreaterThanOrEqual(0,
(int) $redis->config('GET', 'ts-retention-policy')['ts-retention-policy']);
$this->assertGreaterThanOrEqual(0,
(int) $redis->config('GET', 'bf-error-rate')['bf-error-rate']);
$this->assertGreaterThan(0,
(int) $redis->config('GET', 'cf-initial-size')['cf-initial-size']);
$this->expectException(ServerException::class);
$redis->config('SET', 'search-max-doctablesize', 10000);
}
/**
* @group connected
* @requiresRedisVersion >= 2.0.0
@@ -0,0 +1,69 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
class HGETDEL_Test extends PredisCommandTestCase
{
/**
* {@inheritDoc}
*/
protected function getExpectedCommand(): string
{
return HGETDEL::class;
}
/**
* {@inheritDoc}
*/
protected function getExpectedId(): string
{
return 'HGETDEL';
}
/**
* @group disconnected
*/
public function testFilterArguments(): void
{
$command = $this->getCommand();
$command->setArguments(['key', ['field1', 'field2']]);
$this->assertSame(['key', 'FIELDS', 2, 'field1', 'field2'], $command->getArguments());
}
/**
* @group disconnected
*/
public function testParseResponse(): void
{
$command = $this->getCommand();
$this->assertSame(0, $command->parseResponse(0));
$this->assertSame(1, $command->parseResponse(1));
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testReturnsAndRemoveFieldsFromHash(): void
{
$redis = $this->getClient();
$redis->hset('hashkey', 'field1', 'value1', 'field2', 'value2', 'field3', 'value3');
$this->assertSame(['value1', 'value2'], $redis->hgetdel('hashkey', ['field1', 'field2']));
$this->assertSame(['field3' => 'value3'], $redis->hgetall('hashkey'));
}
}
+193
View File
@@ -0,0 +1,193 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
use UnexpectedValueException;
class HGETEX_Test extends PredisCommandTestCase
{
/**
* {@inheritdoc}
*/
protected function getExpectedCommand(): string
{
return HGETEX::class;
}
/**
* {@inheritdoc}
*/
protected function getExpectedId(): string
{
return 'HGETEX';
}
/**
* @group disconnected
* @dataProvider argumentsProvider
*/
public function testFilterArguments(array $actualArguments, array $expectedArguments): void
{
$command = $this->getCommand();
$command->setArguments($actualArguments);
$this->assertSame($expectedArguments, $command->getArguments());
}
/**
* @group disconnected
*/
public function testParseResponse(): void
{
$command = $this->getCommand();
$this->assertSame(1, $command->parseResponse(1));
}
/**
* @group connected
* @dataProvider hashProvider
* @param array $hash
* @param array $arguments
* @param array $expectedResponse
* @param float $timeout
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testReturnsValueAndSetExpirationTimeForGivenHash(
array $hash,
array $arguments,
array $expectedResponse,
float $timeout
): void {
$redis = $this->getClient();
$redis->hset(...$hash);
$this->assertSame($expectedResponse, $redis->hgetex(...$arguments));
$this->sleep($timeout);
$this->assertSame([], $redis->hgetall('hash_key'));
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testRemovesAssociatedTTLFromHash()
{
$redis = $this->getClient();
$redis->hsetex(
'hash_key', ['field1' => 'value1'],
HSETEX::SET_NULL, HSETEX::TTL_EX, 100
);
$this->assertGreaterThan(0, $redis->hexpiretime('hash_key', ['field1'])[0]);
$redis->hgetex('hash_key', ['field1'], HGETEX::PERSIST);
$this->assertEquals(-1, $redis->hexpiretime('hash_key', ['field1'])[0]);
}
/**
* @group connected
* @dataProvider unexpectedValuesProvider
* @param array $arguments
* @param string $expectedExceptionMessage
* @return void
*/
public function testThrowsExceptionOnUnexpectedValueGiven(
array $arguments,
string $expectedExceptionMessage
): void {
$redis = $this->getClient();
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage($expectedExceptionMessage);
$redis->hgetex(...$arguments);
}
public function argumentsProvider(): array
{
return [
'with default arguments' => [
['key', ['field1', 'field2']],
['key', 'FIELDS', 2, 'field1', 'field2'],
],
'with EX modifier' => [
['key', ['field1', 'field2'], HGETEX::EX, 10],
['key', 'EX', 10, 'FIELDS', 2, 'field1', 'field2'],
],
'with PX modifier' => [
['key', ['field1', 'field2'], HGETEX::PX, 10],
['key', 'PX', 10, 'FIELDS', 2, 'field1', 'field2'],
],
'with EXAT modifier' => [
['key', ['field1', 'field2'], HGETEX::EXAT, 10],
['key', 'EXAT', 10, 'FIELDS', 2, 'field1', 'field2'],
],
'with PXAT modifier' => [
['key', ['field1', 'field2'], HGETEX::PXAT, 10],
['key', 'PXAT', 10, 'FIELDS', 2, 'field1', 'field2'],
],
'with PERSIST modifier' => [
['key', ['field1', 'field2'], HGETEX::PERSIST],
['key', 'PERSIST', 'FIELDS', 2, 'field1', 'field2'],
],
];
}
public function hashProvider(): array
{
return [
'with expiration - EX modifier' => [
['hash_key', 'field1', 'value1', 'field2', 'value2'],
['hash_key', ['field1', 'field2'], HGETEX::EX, 1],
['value1', 'value2'],
1.2,
],
'with expiration - PX modifier' => [
['hash_key', 'field1', 'value1', 'field2', 'value2'],
['hash_key', ['field1', 'field2'], HGETEX::PX, 100],
['value1', 'value2'],
0.2,
],
'with expiration - EXAT modifier' => [
['hash_key', 'field1', 'value1', 'field2', 'value2'],
['hash_key', ['field1', 'field2'], HGETEX::EXAT, time() + 1],
['value1', 'value2'],
2,
],
'with expiration - PXAT modifier' => [
['hash_key', 'field1', 'value1', 'field2', 'value2'],
['hash_key', ['field1', 'field2'], HGETEX::PXAT, (time() * 1000) + 100],
['value1', 'value2'],
0.3,
],
];
}
public function unexpectedValuesProvider(): array
{
return [
'with wrong modifier' => [
['key', ['field1', 'field2'], 'wrong', 10],
'Modifier argument accepts only: ex, px, exat, pxat, persist values',
],
'with wrong type value' => [
['key', ['field1', 'field2'], 'ex', true],
'Modifier value is missing or incorrect type',
],
];
}
}
+257
View File
@@ -0,0 +1,257 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
use UnexpectedValueException;
class HSETEX_Test extends PredisCommandTestCase
{
/**
* {@inheritdoc}
*/
protected function getExpectedCommand(): string
{
return HSETEX::class;
}
/**
* {@inheritdoc}
*/
protected function getExpectedId(): string
{
return 'HSETEX';
}
/**
* @group disconnected
* @dataProvider argumentsProvider
*/
public function testFilterArguments(array $actualArguments, array $expectedArguments): void
{
$command = $this->getCommand();
$command->setArguments($actualArguments);
$this->assertSame($expectedArguments, $command->getArguments());
}
/**
* @group connected
* @group slow
* @dataProvider hashProvider
* @param array $arguments
* @param int $expectedResponse
* @param float $timeout
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testSetHashWithExpiration(
array $arguments,
int $expectedResponse,
float $timeout
): void {
$redis = $this->getClient();
$this->assertSame($expectedResponse, $redis->hsetex(...$arguments));
$this->sleep($timeout);
$this->assertSame([], $redis->hgetall('hash_key'));
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testSetHashFieldsIfNotExists(): void
{
$redis = $this->getClient();
$this->assertSame(
1,
$redis->hsetex('hash_key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_FNX)
);
$this->assertSame(
0,
$redis->hsetex('hash_key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_FNX)
);
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testSetHashFieldsOnlyIfExists(): void
{
$redis = $this->getClient();
$this->assertSame(
0,
$redis->hsetex('hash_key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_FXX)
);
$this->assertSame(
1,
$redis->hsetex('hash_key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_FNX)
);
$this->assertSame(
1,
$redis->hsetex('hash_key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_FXX)
);
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testSetHashFieldRetainingTTLValue(): void
{
$redis = $this->getClient();
$this->assertSame(
1,
$redis->hsetex(
'hash_key',
['field1' => 'value1', 'field2' => 'value2'],
HSETEX::SET_FNX,
HSETEX::TTL_EX,
100
)
);
$this->assertGreaterThan(0, $redis->hexpiretime('hash_key', ['field1'])[0]);
$this->assertSame(
1,
$redis->hsetex(
'hash_key',
['field1' => 'value1'],
HSETEX::SET_FXX,
HSETEX::TTL_KEEP_TTL
)
);
$this->assertGreaterThan(0, $redis->hexpiretime('hash_key', ['field1'])[0]);
}
/**
* @group connected
* @dataProvider unexpectedValuesProvider
* @param array $arguments
* @param string $expectedExceptionMessage
* @return void
*/
public function testThrowsExceptionOnUnexpectedValueGiven(
array $arguments,
string $expectedExceptionMessage
): void {
$redis = $this->getClient();
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage($expectedExceptionMessage);
$redis->hsetex(...$arguments);
}
public function argumentsProvider(): array
{
return [
'with default arguments' => [
['key', ['field1' => 'value1', 'field2' => 'value2']],
['key', 'FIELDS', 2, 'field1', 'value1', 'field2', 'value2'],
],
'with FNX modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_FNX],
['key', 'FNX', 'FIELDS', 2, 'field1', 'value1', 'field2', 'value2'],
],
'with FXX modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_FXX],
['key', 'FXX', 'FIELDS', 2, 'field1', 'value1', 'field2', 'value2'],
],
'with EX modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_NULL, HSETEX::TTL_EX, 10],
['key', 'EX', 10, 'FIELDS', 2, 'field1', 'value1', 'field2', 'value2'],
],
'with PX modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_NULL, HSETEX::TTL_PX, 10],
['key', 'PX', 10, 'FIELDS', 2, 'field1', 'value1', 'field2', 'value2'],
],
'with EXAT modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_NULL, HSETEX::TTL_EXAT, 10],
['key', 'EXAT', 10, 'FIELDS', 2, 'field1', 'value1', 'field2', 'value2'],
],
'with PXAT modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_NULL, HSETEX::TTL_PXAT, 10],
['key', 'PXAT', 10, 'FIELDS', 2, 'field1', 'value1', 'field2', 'value2'],
],
'with KEEPTTL modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_NULL, HSETEX::TTL_KEEP_TTL],
['key', 'KEEPTTL', 'FIELDS', 2, 'field1', 'value1', 'field2', 'value2'],
],
'with combined modifiers' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_FXX, HSETEX::TTL_PXAT, 10],
['key', 'FXX', 'PXAT', 10, 'FIELDS', 2, 'field1', 'value1', 'field2', 'value2'],
],
];
}
public function hashProvider(): array
{
return [
'with EX modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_NULL, HSETEX::TTL_EX, 1],
1,
1.2,
],
'with PX modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_NULL, HSETEX::TTL_PX, 100],
1,
0.2,
],
'with EXAT modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_NULL, HSETEX::TTL_EXAT, time() + 1],
1,
2,
],
'with PXAT modifier' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_NULL, HSETEX::TTL_PXAT, (time() * 1000) + 100],
1,
0.3,
],
'with combined modifiers' => [
['key', ['field1' => 'value1', 'field2' => 'value2'], HSETEX::SET_FNX, HSETEX::TTL_PX, 100],
1,
0.2,
],
];
}
public function unexpectedValuesProvider(): array
{
return [
'with wrong set modifier' => [
['key', ['field1', 'field2'], 'wrong'],
'Modifier argument accepts only: fnx, fxx values',
],
'with wrong ttl modifier' => [
['key', ['field1', 'field2'], '', 'wrong'],
'Modifier argument accepts only: ex, px, exat, pxat, keepttl values',
],
'with wrong ttl modifier value' => [
['key', ['field1', 'field2'], '', HSETEX::TTL_PXAT, 'wrong'],
'Modifier value is missing or incorrect type',
],
];
}
}
+15
View File
@@ -313,6 +313,21 @@ BUFFER;
$this->assertArrayHasKey('redis_version', $info['Server'] ?? $info);
}
/**
* @group connected
* @requiresRedisVersion >= 7.9.0
*/
public function testExposeSearchInformation(): void
{
$this->markTestSkipped('Skipped due to a bug in 8.0-M05. Should be removed in the next version.');
$redis = $this->getClient();
$this->assertArrayHasKey('search', $redis->info('modules')['Modules']);
$this->assertNotEmpty($redis->info('search'));
$this->assertArrayHasKey('search', $redis->info('everything')['Modules']);
}
/**
* @group connected
* @requiresRedisVersion < 2.6.0
@@ -124,7 +124,6 @@ class FTAGGREGATE_Test extends PredisCommandTestCase
$redis = $this->getClient();
$this->expectException(ServerException::class);
$this->expectExceptionMessage('index: no such index');
$redis->ftaggregate('index', 'query');
}
@@ -134,55 +133,55 @@ class FTAGGREGATE_Test extends PredisCommandTestCase
return [
'with default arguments' => [
['index', 'query'],
['index', 'query', 'DIALECT', 2],
['index', 'query'],
],
'with VERBATIM modifier' => [
['index', 'query', (new AggregateArguments())->verbatim()],
['index', 'query', 'VERBATIM', 'DIALECT', 2],
['index', 'query', 'VERBATIM'],
],
'with LOAD modifier - specified fields' => [
['index', 'query', (new AggregateArguments())->load('field1', 'field2')],
['index', 'query', 'LOAD', 2, 'field1', 'field2', 'DIALECT', 2],
['index', 'query', 'LOAD', 2, 'field1', 'field2'],
],
'with LOAD modifier - all fields' => [
['index', 'query', (new AggregateArguments())->load('*')],
['index', 'query', 'LOAD', '*', 'DIALECT', 2],
['index', 'query', 'LOAD', '*'],
],
'with TIMEOUT modifier' => [
['index', 'query', (new AggregateArguments())->timeout(2)],
['index', 'query', 'TIMEOUT', 2, 'DIALECT', 2],
['index', 'query', 'TIMEOUT', 2],
],
'with GROUPBY modifier' => [
['index', 'query', (new AggregateArguments())->groupBy('property1', 'property2')],
['index', 'query', 'GROUPBY', 2, 'property1', 'property2', 'DIALECT', 2],
['index', 'query', 'GROUPBY', 2, 'property1', 'property2'],
],
'with REDUCE modifier' => [
['index', 'query', (new AggregateArguments())->reduce('function', 'arg1', true, 'alias1', 'arg2')],
['index', 'query', 'REDUCE', 'function', 2, 'arg1', 'AS', 'alias1', 'arg2', 'DIALECT', 2],
['index', 'query', 'REDUCE', 'function', 2, 'arg1', 'AS', 'alias1', 'arg2'],
],
'with SORTBY modifier' => [
['index', 'query', (new AggregateArguments())->sortBy(2, 'property1', 'ASC', 'property2', 'DESC')],
['index', 'query', 'SORTBY', 2, 'property1', 'ASC', 'property2', 'DESC', 'MAX', 2, 'DIALECT', 2],
['index', 'query', 'SORTBY', 2, 'property1', 'ASC', 'property2', 'DESC', 'MAX', 2],
],
'with APPLY modifier' => [
['index', 'query', (new AggregateArguments())->apply('expression', 'name')],
['index', 'query', 'APPLY', 'expression', 'AS', 'name', 'DIALECT', 2],
['index', 'query', 'APPLY', 'expression', 'AS', 'name'],
],
'with LIMIT modifier' => [
['index', 'query', (new AggregateArguments())->limit(2, 3)],
['index', 'query', 'LIMIT', 2, 3, 'DIALECT', 2],
['index', 'query', 'LIMIT', 2, 3],
],
'with FILTER modifier' => [
['index', 'query', (new AggregateArguments())->filter('filter')],
['index', 'query', 'FILTER', 'filter', 'DIALECT', 2],
['index', 'query', 'FILTER', 'filter'],
],
'with WITHCURSOR modifier' => [
['index', 'query', (new AggregateArguments())->withCursor(10, 20)],
['index', 'query', 'WITHCURSOR', 'COUNT', 10, 'MAXIDLE', 20, 'DIALECT', 2],
['index', 'query', 'WITHCURSOR', 'COUNT', 10, 'MAXIDLE', 20],
],
'with PARAMS modifier' => [
['index', 'query', (new AggregateArguments())->params(['name1', 'value1', 'name2', 'value2'])],
['index', 'query', 'PARAMS', 4, 'name1', 'value1', 'name2', 'value2', 'DIALECT', 2],
['index', 'query', 'PARAMS', 4, 'name1', 'value1', 'name2', 'value2'],
],
'with DIALECT modifier' => [
['index', 'query', (new AggregateArguments())->dialect('dialect')],
@@ -200,7 +199,7 @@ class FTAGGREGATE_Test extends PredisCommandTestCase
],
[
'index', '@name: "test"', 'APPLY', 'year(@dob)', 'AS', 'birth', 'GROUPBY', 2, '@birth', '@country',
'REDUCE', 'COUNT', 0, 'AS', 'num_visits', 'SORTBY', 1, '@day', 'DIALECT', 2,
'REDUCE', 'COUNT', 0, 'AS', 'num_visits', 'SORTBY', 1, '@day',
],
],
];
@@ -94,7 +94,6 @@ class FTALIASADD_Test extends PredisCommandTestCase
$redis = $this->getClient();
$this->expectException(ServerException::class);
$this->expectExceptionMessage('Unknown index name (or name is an alias itself)');
$redis->ftaliasadd('alias', 'index');
}
@@ -109,7 +109,6 @@ class FTALIASUPDATE_Test extends PredisCommandTestCase
$redis = $this->getClient();
$this->expectException(ServerException::class);
$this->expectExceptionMessage('Unknown index name (or name is an alias itself)');
$redis->ftaliasupdate('alias', 'index');
}
@@ -88,7 +88,6 @@ class FTALTER_Test extends PredisCommandTestCase
$redis = $this->getClient();
$this->expectException(ServerException::class);
$this->expectExceptionMessage('Unknown index name');
$redis->ftalter('alias', [new TextField('field_name')]);
}
@@ -91,7 +91,7 @@ class FTCONFIG_Test extends PredisCommandTestCase
* @group connected
* @group relay-resp3
* @return void
* @requiresRedisVersion <= 7.3.0
* @requiresRedisVersion <= 7.0.0
*/
public function testSetGivenRediSearchConfigurationParameter(): void
{
@@ -104,7 +104,7 @@ class FTCONFIG_Test extends PredisCommandTestCase
* @group connected
* @group relay-resp3
* @return void
* @requiresRedisVersion <= 7.3.0
* @requiresRedisVersion <= 7.0.0
*/
public function testGetReturnsGivenRediSearchConfigurationParameter(): void
{
@@ -118,7 +118,7 @@ class FTCONFIG_Test extends PredisCommandTestCase
* @group connected
* @group relay-resp3
* @return void
* @requiresRedisVersion <= 7.3.0
* @requiresRedisVersion <= 7.0.0
*/
public function testHelpReturnsGivenRediSearchConfigurationDescription(): void
{
@@ -141,14 +141,13 @@ class FTCONFIG_Test extends PredisCommandTestCase
* @group connected
* @group relay-resp3
* @return void
* @requiresRedisVersion <= 7.3.0
* @requiresRedisVersion <= 7.0.0
*/
public function testSetThrowsExceptionOnNonExistingOption(): void
{
$redis = $this->getClient();
$this->expectException(ServerException::class);
$this->expectExceptionMessage('Invalid option');
$redis->ftconfig->set('foobar', 'value');
}
@@ -188,7 +188,6 @@ class FTCURSOR_Test extends PredisCommandTestCase
$redis = $this->getClient();
$this->expectException(ServerException::class);
$this->expectExceptionMessage('Cursor not found');
$redis->ftcursor->read('idx', 21412412);
}
@@ -204,7 +203,6 @@ class FTCURSOR_Test extends PredisCommandTestCase
$redis = $this->getClient();
$this->expectException(ServerException::class);
$this->expectExceptionMessage('Cursor does not exist');
$redis->ftcursor->del('idx', 21412412);
}
@@ -78,13 +78,13 @@ class FTDICTDUMP_Test extends PredisCommandTestCase
* @group connected
* @return void
* @requiresRediSearchVersion >= 1.4.0
* @requiresRedisVersion <= 7.9.0
*/
public function testThrowsExceptionOnNonExistingDictionary(): void
{
$redis = $this->getClient();
$this->expectException(ServerException::class);
$this->expectExceptionMessage('could not open dict key');
$redis->ftdictdump('dict');
}
@@ -85,7 +85,6 @@ class FTDROPINDEX_Test extends PredisCommandTestCase
$redis = $this->getClient();
$this->expectException(ServerException::class);
$this->expectExceptionMessage('Unknown Index name');
$redis->ftdropindex('index');
}
@@ -64,6 +64,7 @@ class FTEXPLAIN_Test extends PredisCommandTestCase
* @group relay-resp3
* @return void
* @requiresRediSearchVersion >= 1.0.0
* @requiresRedisVersion <= 7.9.0
*/
public function testExplainReturnsExecutionPlanForGivenQuery(): void
{
@@ -105,10 +106,7 @@ EOT;
$this->assertEquals('OK', $redis->ftcreate('index', $schema));
$this->assertEquals(
$expectedResponse,
$redis->ftexplain(
'index', '(foo bar)|(hello world) @date:[100 200]|@date:[500 +inf]',
(new ExplainArguments())->dialect(1)
)
$redis->ftexplain('index', '(foo bar)|(hello world) @date:[100 200]|@date:[500 +inf]')
);
}
@@ -122,7 +120,6 @@ EOT;
$redis = $this->getClient();
$this->expectException(ServerException::class);
$this->expectExceptionMessage('index: no such index');
$redis->ftexplain('index', 'query');
}
@@ -132,7 +129,7 @@ EOT;
return [
'with default arguments' => [
['index', 'query', null],
['index', 'query', 'DIALECT', 2],
['index', 'query'],
],
'with DIALECT' => [
['index', 'query', (new ExplainArguments())->dialect('dialect')],
@@ -88,7 +88,6 @@ class FTPROFILE_Test extends PredisCommandTestCase
$redis = $this->getClient();
$this->expectException(ServerException::class);
$this->expectExceptionMessage('index: no such index');
$redis->ftprofile('index', (new ProfileArguments())->search()->query('query'));
}
@@ -416,36 +416,31 @@ class FTSEARCH_Test extends PredisCommandTestCase
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testVectorSearchWithDefaultDialect(): void
public function testVectorSearchWithInt8Types(): void
{
$redis = $this->getClient();
$this->assertEquals('OK', $redis->ftcreate('test', [
new VectorField(
'v', 'HNSW',
["TYPE", "FLOAT32", "DIM", 2, "DISTANCE_METRIC", "L2"]
)
['TYPE', 'INT8', 'DIM', 2, 'DISTANCE_METRIC', 'L2']
),
]));
$this->sleep(0.1);
$redis->hset("a", "v", "aaaaaaaa");
$redis->hset("b", "v", "aaaabaaa");
$redis->hset("c", "v", "aaaaabaa");
$a = [1.5, 10];
$b = [123, 100];
$c = [1, 1];
$redis->hset('a', 'v', pack('c*', ...$a));
$redis->hset('b', 'v', pack('c*', ...$b));
$redis->hset('c', 'v', pack('c*', ...$c));
$searchArguments = new SearchArguments();
$searchArguments->params(["vec", "aaaaaaaa"]);
$searchArguments->params(['vec', pack('c*', ...$a)]);
$searchArguments->dialect(2);
$ftSearch = new FTSEARCH();
$ftSearch->setArguments(['test', '*=>[KNN 2 @v $vec]', $searchArguments]);
$this->assertContains('DIALECT', $ftSearch->getArguments());
$this->assertContains(2, $ftSearch->getArguments());
// Check if dialect applied via constructing custom command object.
$this->assertEquals(2, $redis->executeCommand($ftSearch)[0]);
// Check if it works via usual client interface.
$this->assertEquals(
2,
$redis->ftsearch('test', '*=>[KNN 2 @v $vec]', $searchArguments)[0]
@@ -458,32 +453,34 @@ class FTSEARCH_Test extends PredisCommandTestCase
* @return void
* @requiresRedisVersion >= 7.9.0
*/
public function testSearchQueryWithDifferentDialects(): void
public function testVectorSearchWithUInt8Types(): void
{
$redis = $this->getClient();
$createArguments = new CreateArguments();
$createArguments->prefix(['test:']);
$this->assertEquals('OK', $redis->ftcreate('test', [
new TextField("name"),
new TextField("lastname"),
], $createArguments));
new VectorField(
'v', 'HNSW',
['TYPE', 'UINT8', 'DIM', 2, 'DISTANCE_METRIC', 'L2']
),
]));
$this->sleep(0.1);
$redis->hset("test:1", "name", "James");
$redis->hset("test:1", "lastname", "Brown");
$a = [1.5, 10];
$b = [123, 100];
$c = [1, 1];
# Query with default DIALECT 2
$this->assertEquals(1, $redis->ftsearch('test', '@name: James Brown')[0]);
$redis->hset('a', 'v', pack('C*', ...$a));
$redis->hset('b', 'v', pack('C*', ...$b));
$redis->hset('c', 'v', pack('C*', ...$c));
$searchArguments = new SearchArguments();
$searchArguments->dialect(1);
$searchArguments->params(['vec', pack('C*', ...$a)]);
$searchArguments->dialect(2);
# Query with explicit DIALECT 1
$this->assertEquals(
0,
$redis->ftsearch('test', '@name: James Brown', $searchArguments)[0]
2,
$redis->ftsearch('test', '*=>[KNN 2 @v $vec]', $searchArguments)[0]
);
}
@@ -492,91 +489,91 @@ class FTSEARCH_Test extends PredisCommandTestCase
return [
'with NOCONTENT modifier' => [
['index', '*', (new SearchArguments())->noContent()],
['index', '*', 'NOCONTENT', 'DIALECT', 2],
['index', '*', 'NOCONTENT'],
],
'with VERBATIM modifier' => [
['index', '*', (new SearchArguments())->verbatim()],
['index', '*', 'VERBATIM', 'DIALECT', 2],
['index', '*', 'VERBATIM'],
],
'with WITHSCORES modifier' => [
['index', '*', (new SearchArguments())->withScores()],
['index', '*', 'WITHSCORES', 'DIALECT', 2],
['index', '*', 'WITHSCORES'],
],
'with WITHPAYLOADS modifier' => [
['index', '*', (new SearchArguments())->withPayloads()],
['index', '*', 'WITHPAYLOADS', 'DIALECT', 2],
['index', '*', 'WITHPAYLOADS'],
],
'with WITHSORTKEYS modifier' => [
['index', '*', (new SearchArguments())->withSortKeys()],
['index', '*', 'WITHSORTKEYS', 'DIALECT', 2],
['index', '*', 'WITHSORTKEYS'],
],
'with FILTER modifier' => [
['index', '*', (new SearchArguments())->searchFilter(['numeric_field', 1, 10])],
['index', '*', 'FILTER', 'numeric_field', 1, 10, 'DIALECT', 2],
['index', '*', 'FILTER', 'numeric_field', 1, 10],
],
'with GEOFILTER modifier' => [
['index', '*', (new SearchArguments())->geoFilter(['geo_field', 12.213, 14.212, 300, 'km'])],
['index', '*', 'GEOFILTER', 'geo_field', 12.213, 14.212, 300, 'km', 'DIALECT', 2],
['index', '*', 'GEOFILTER', 'geo_field', 12.213, 14.212, 300, 'km'],
],
'with INKEYS modifier' => [
['index', '*', (new SearchArguments())->inKeys(['key1', 'key2'])],
['index', '*', 'INKEYS', 2, 'key1', 'key2', 'DIALECT', 2],
['index', '*', 'INKEYS', 2, 'key1', 'key2'],
],
'with INFIELDS modifier' => [
['index', '*', (new SearchArguments())->inFields(['field1', 'field2'])],
['index', '*', 'INFIELDS', 2, 'field1', 'field2', 'DIALECT', 2],
['index', '*', 'INFIELDS', 2, 'field1', 'field2'],
],
'with RETURN modifier' => [
['index', '*', (new SearchArguments())->addReturn(2, 'identifier', true, 'property')],
['index', '*', 'RETURN', 2, 'identifier', 'AS', 'property', 'DIALECT', 2],
['index', '*', 'RETURN', 2, 'identifier', 'AS', 'property'],
],
'with SUMMARIZE modifier' => [
['index', '*', (new SearchArguments())->summarize(['field1', 'field2'], 2, 2, ',')],
['index', '*', 'SUMMARIZE', 'FIELDS', 2, 'field1', 'field2', 'FRAGS', 2, 'LEN', 2, 'SEPARATOR', ',', 'DIALECT', 2],
['index', '*', 'SUMMARIZE', 'FIELDS', 2, 'field1', 'field2', 'FRAGS', 2, 'LEN', 2, 'SEPARATOR', ','],
],
'with HIGHLIGHT modifier' => [
['index', '*', (new SearchArguments())->highlight(['field1', 'field2'], 'openTag', 'closeTag')],
['index', '*', 'HIGHLIGHT', 'FIELDS', 2, 'field1', 'field2', 'TAGS', 'openTag', 'closeTag', 'DIALECT', 2],
['index', '*', 'HIGHLIGHT', 'FIELDS', 2, 'field1', 'field2', 'TAGS', 'openTag', 'closeTag'],
],
'with SLOP modifier' => [
['index', '*', (new SearchArguments())->slop(2)],
['index', '*', 'SLOP', 2, 'DIALECT', 2],
['index', '*', 'SLOP', 2],
],
'with TIMEOUT modifier' => [
['index', '*', (new SearchArguments())->timeout(2)],
['index', '*', 'TIMEOUT', 2, 'DIALECT', 2],
['index', '*', 'TIMEOUT', 2],
],
'with INORDER modifier' => [
['index', '*', (new SearchArguments())->inOrder()],
['index', '*', 'INORDER', 'DIALECT', 2],
['index', '*', 'INORDER'],
],
'with EXPANDER modifier' => [
['index', '*', (new SearchArguments())->expander('expander')],
['index', '*', 'EXPANDER', 'expander', 'DIALECT', 2],
['index', '*', 'EXPANDER', 'expander'],
],
'with SCORER modifier' => [
['index', '*', (new SearchArguments())->scorer('scorer')],
['index', '*', 'SCORER', 'scorer', 'DIALECT', 2],
['index', '*', 'SCORER', 'scorer'],
],
'with EXPLAINSCORE modifier' => [
['index', '*', (new SearchArguments())->explainScore()],
['index', '*', 'EXPLAINSCORE', 'DIALECT', 2],
['index', '*', 'EXPLAINSCORE'],
],
'with PAYLOAD modifier' => [
['index', '*', (new SearchArguments())->payload('payload')],
['index', '*', 'PAYLOAD', 'payload', 'DIALECT', 2],
['index', '*', 'PAYLOAD', 'payload'],
],
'with SORTBY modifier' => [
['index', '*', (new SearchArguments())->sortBy('sort_attribute', 'desc')],
['index', '*', 'SORTBY', 'sort_attribute', 'DESC', 'DIALECT', 2],
['index', '*', 'SORTBY', 'sort_attribute', 'DESC'],
],
'with LIMIT modifier' => [
['index', '*', (new SearchArguments())->limit(2, 2)],
['index', '*', 'LIMIT', 2, 2, 'DIALECT', 2],
['index', '*', 'LIMIT', 2, 2],
],
'with PARAMS modifier' => [
['index', '*', (new SearchArguments())->params(['name1', 'value2', 'name2', 'value2'])],
['index', '*', 'PARAMS', 4, 'name1', 'value2', 'name2', 'value2', 'DIALECT', 2],
['index', '*', 'PARAMS', 4, 'name1', 'value2', 'name2', 'value2'],
],
'with DIALECT modifier' => [
['index', '*', (new SearchArguments())->dialect('dialect')],
@@ -584,7 +581,7 @@ class FTSEARCH_Test extends PredisCommandTestCase
],
'with chain of arguments' => [
['index', '*', (new SearchArguments())->withScores()->withPayloads()->searchFilter(['numeric_field', 1, 10])->addReturn(2, 'identifier', true, 'property')],
['index', '*', 'WITHSCORES', 'WITHPAYLOADS', 'FILTER', 'numeric_field', 1, 10, 'RETURN', 2, 'identifier', 'AS', 'property', 'DIALECT', 2],
['index', '*', 'WITHSCORES', 'WITHPAYLOADS', 'FILTER', 'numeric_field', 1, 10, 'RETURN', 2, 'identifier', 'AS', 'property'],
],
];
}
@@ -129,19 +129,19 @@ class FTSPELLCHECK_Test extends PredisCommandTestCase
return [
'with default arguments' => [
['index', 'query'],
['index', 'query', 'DIALECT', 2],
['index', 'query'],
],
'with DISTANCE modifier' => [
['index', 'query', (new SpellcheckArguments())->distance(2)],
['index', 'query', 'DISTANCE', 2, 'DIALECT', 2],
['index', 'query', 'DISTANCE', 2],
],
'with TERMS modifier - INCLUDE' => [
['index', 'query', (new SpellcheckArguments())->terms('dict', 'INCLUDE', 'term')],
['index', 'query', 'TERMS', 'INCLUDE', 'dict', 'term', 'DIALECT', 2],
['index', 'query', 'TERMS', 'INCLUDE', 'dict', 'term'],
],
'with TERMS modifier - EXCLUDE' => [
['index', 'query', (new SpellcheckArguments())->terms('dict', 'EXCLUDE', 'term')],
['index', 'query', 'TERMS', 'EXCLUDE', 'dict', 'term', 'DIALECT', 2],
['index', 'query', 'TERMS', 'EXCLUDE', 'dict', 'term'],
],
'with DIALECT modifier' => [
['index', 'query', (new SpellcheckArguments())->dialect('dialect')],
@@ -105,11 +105,11 @@ class FTSUGGET_Test extends PredisCommandTestCase
* @return void
* @requiresRediSearchVersion >= 1.0.0
*/
public function testGetReturnsNullOnNonExistingKey(): void
public function testGetReturnsEmptyArrayOnNonExistingKey(): void
{
$redis = $this->getClient();
$this->assertNull($redis->ftsugget('key', 'hel'));
$this->assertEmpty($redis->ftsugget('key', 'hel'));
}
public function argumentsProvider(): array
@@ -94,7 +94,6 @@ class FTSYNDUMP_Test extends PredisCommandTestCase
$redis = $this->getClient();
$this->expectException(ServerException::class);
$this->expectExceptionMessage('Unknown index name');
$redis->ftsyndump('index');
}
@@ -116,7 +116,6 @@ class FTSYNUPDATE_Test extends PredisCommandTestCase
$redis = $this->getClient();
$this->expectException(ServerException::class);
$this->expectExceptionMessage('Unknown index name');
$redis->ftsynupdate(
'index',
@@ -104,7 +104,6 @@ class FTTAGVALS_Test extends PredisCommandTestCase
$redis = $this->getClient();
$this->expectException(ServerException::class);
$this->expectExceptionMessage('Unknown Index name');
$redis->fttagvals('index', 'fieldName');
}
@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis\Search;
use Predis\Command\Argument\Search\SchemaFields\TextField;
use Predis\Command\Redis\PredisCommandTestCase;
class FT_LIST_Test extends PredisCommandTestCase
{
/**
* {@inheritDoc}
*/
protected function getExpectedCommand(): string
{
return FT_LIST::class;
}
/**
* {@inheritDoc}
*/
protected function getExpectedId(): string
{
return 'FT_LIST';
}
/**
* @group disconnected
*/
public function testFilterArguments(): void
{
$this->assertEmpty($this->getCommand()->getArguments());
}
/**
* @group connected
* @return void
* @requiresRediSearchVersion >= 2.0.0
*/
public function testReturnListOfExistingIndices(): void
{
$redis = $this->getClient();
$this->assertEquals('OK', $redis->ftcreate('idx1', [new TextField('text')]));
$this->assertEquals('OK', $redis->ftcreate('idx2', [new TextField('text')]));
$this->sleep(0.1);
$this->assertSameValues(['idx1', 'idx2'], $redis->ft_list());
}
}
@@ -64,11 +64,12 @@ class TSINFO_Test extends PredisCommandTestCase
* @group relay-resp3
* @return void
* @requiresRedisTimeSeriesVersion <= 1.10.13
* @requiresRedisVersion > 6.3.0
*/
public function testReturnsInformationAboutGivenTimeSeries(): void
{
$redis = $this->getClient();
$expectedResponse = ['totalSamples', 0, 'memoryUsage', 4239, 'firstTimestamp', 0, 'lastTimestamp', 0,
$expectedResponse = ['totalSamples', 0, 'memoryUsage', 5000, 'firstTimestamp', 0, 'lastTimestamp', 0,
'retentionTime', 60000, 'chunkCount', 1, 'chunkSize', 4096, 'chunkType', 'compressed', 'duplicatePolicy',
'max', 'labels', [['sensor_id', '2'], ['area_id', '32']], 'sourceKey', null, 'rules', [],
'ignoreMaxTimeDiff', 0, 'ignoreMaxValDiff', 0];
@@ -83,7 +84,7 @@ class TSINFO_Test extends PredisCommandTestCase
$redis->tscreate('temperature:2:32', $arguments)
);
$this->assertEquals($expectedResponse, $redis->tsinfo('temperature:2:32'));
$this->assertEqualsWithDelta($expectedResponse, $redis->tsinfo('temperature:2:32'), 1000);
}
public function argumentsProvider(): array
@@ -319,12 +319,14 @@ class RedisClusterTest extends PredisTestCase
->withConsecutive(
[
[
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => '6383',
],
],
[
[
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => '6384',
],
@@ -644,6 +646,7 @@ class RedisClusterTest extends PredisTestCase
->expects($this->once())
->method('create')
->with([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => '9381',
])
@@ -710,6 +713,7 @@ class RedisClusterTest extends PredisTestCase
->expects($this->once())
->method('create')
->with([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => '9381',
])
@@ -1020,6 +1024,7 @@ class RedisClusterTest extends PredisTestCase
->expects($this->once())
->method('create')
->with([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => '6381',
])
@@ -1108,6 +1113,58 @@ class RedisClusterTest extends PredisTestCase
->expects($this->once())
->method('create')
->with([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => '6381',
])
->willReturn($connection3);
$cluster = new RedisCluster($factory);
$cluster->useClusterSlots(false);
$cluster->add($connection1);
$cluster->add($connection2);
$this->assertSame('foobar', $cluster->executeCommand($command));
$this->assertSame('foobar', $cluster->executeCommand($command));
$this->assertCount(3, $cluster);
}
/**
* @group disconnected
*/
public function testNotTCPMovedResponseWithConnectionNotInPool(): void
{
$movedResponse = new Response\Error('MOVED 1970 127.0.0.1:6381');
$command = $this->getCommandFactory()->create('get', ['node:1001']);
$connection1 = $this->getMockConnection('tls://127.0.0.1:6379');
$connection1
->expects($this->once())
->method('executeCommand')
->with($command)
->willReturn($movedResponse);
$connection2 = $this->getMockConnection('tls://127.0.0.1:6380');
$connection2
->expects($this->never())
->method('executeCommand');
$connection3 = $this->getMockConnection('tls://127.0.0.1:6381');
$connection3
->expects($this->exactly(2))
->method('executeCommand')
->with($command)
->willReturnOnConsecutiveCalls('foobar', 'foobar');
/** @var Connection\FactoryInterface|MockObject */
$factory = $this->getMockBuilder('Predis\Connection\FactoryInterface')->getMock();
$factory
->expects($this->once())
->method('create')
->with([
'scheme' => 'tls',
'host' => '127.0.0.1',
'port' => '6381',
])
@@ -1153,6 +1210,7 @@ class RedisClusterTest extends PredisTestCase
->expects($this->once())
->method('create')
->with([
'scheme' => 'tcp',
'host' => '2001:db8:0:f101::2',
'port' => '6379',
])
@@ -1250,6 +1308,7 @@ class RedisClusterTest extends PredisTestCase
->expects($this->once())
->method('create')
->with([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => '6380',
])
@@ -123,23 +123,6 @@ class StreamConnectionTest extends PredisConnectionTestCase
$connection1->disconnect();
}
/**
* @group connected
* @requires PHP 5.4
*/
public function testPersistentConnectionsToSameNodeShareResource(): void
{
$connection1 = $this->createConnectionWithParams(['persistent' => true]);
$connection2 = $this->createConnectionWithParams(['persistent' => true]);
$this->assertPersistentConnection($connection1);
$this->assertPersistentConnection($connection2);
$this->assertSame($connection1->getResource(), $connection2->getResource());
$connection1->disconnect();
}
/**
* @group connected
* @requires PHP 5.4