mirror of
https://github.com/predis/predis.git
synced 2026-08-18 21:31:52 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f49e13ee3a | |||
| 6e3e2c0e78 | |||
| c35c422eda | |||
| 1b5ed7d516 | |||
| 5df852f227 | |||
| 9418df1924 | |||
| 0e4b3829a7 | |||
| 65f6127fab | |||
| 7ff24b19ae | |||
| 2babfc91d7 | |||
| fdd52d0284 | |||
| 48a861053e | |||
| 7bf1f6eb29 | |||
| 067b051900 | |||
| df4a318b80 | |||
| a904a3a273 | |||
| 8b240d15a5 | |||
| 02e2a7e1cc |
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
+2
-1
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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],
|
||||
|
||||
@@ -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/
|
||||
*
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -165,6 +165,11 @@ class Parameters implements ParametersInterface
|
||||
}
|
||||
}
|
||||
|
||||
public function __set($parameter, $value)
|
||||
{
|
||||
$this->parameters[$parameter] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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 ------------------------------------------------ //
|
||||
// ******************************************************************** //
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user