Skip to content

Commit 04bf5e9

Browse files
authored
Merge pull request #107 from vemaeg/feature/sscan-implementation
sscan implementation based on existing scan method
2 parents 2177985 + 6b74103 commit 04bf5e9

File tree

3 files changed

+92
-0
lines changed

3 files changed

+92
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Redis command | Description
5353
**RPUSH** *key* *value* | Pushs values at the tail of a list
5454
**RPOP** *key* | Pops values at the tail of a list
5555
**SCAN** | Iterates the set of keys in the currently selected Redis database.
56+
**SSCAN** | Iterates elements of Sets types.
5657
**SET** *key* *value* | Sets the string value of a key
5758
**SETEX** *key* *seconds* *value* | Sets the value and expiration of a key
5859
**SETNX** *key* *value* | Sets key to hold value if key does not exist

src/M6Web/Component/RedisMock/RedisMock.php

+48
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,54 @@ public function sismember($key, $member)
466466
return $this->returnPipedInfo(1);
467467
}
468468

469+
470+
/**
471+
* Mock the `sscan` command
472+
* @see https://redis.io/commands/sscan
473+
* @param string $key
474+
* @param int $cursor
475+
* @param array $options contain options of the command, with values (ex ['MATCH' => 'st*', 'COUNT' => 42] )
476+
* @return $this|array|mixed
477+
*/
478+
public function sscan($key, $cursor = 0, array $options = [])
479+
{
480+
$match = isset($options['MATCH']) ? $options['MATCH'] : '*';
481+
$count = isset($options['COUNT']) ? $options['COUNT'] : 10;
482+
$maximumValue = $cursor + $count -1;
483+
484+
if (!isset(self::$dataValues[$this->storage][$key]) || $this->deleteOnTtlExpired($key)) {
485+
return $this->returnPipedInfo([0, []]);
486+
}
487+
488+
// List of all keys in the storage (already ordered by index).
489+
$set = self::$dataValues[$this->storage][$key];
490+
$maximumListElement = count($set);
491+
492+
// Next cursor position
493+
$nextCursorPosition = 0;
494+
// Matched values.
495+
$values = [];
496+
// Pattern, for find matched values.
497+
$pattern = sprintf('/^%s$/', str_replace(['*', '/'], ['.*', '\/'], $match));
498+
499+
for($i = $cursor; $i <= $maximumValue; $i++)
500+
{
501+
if (isset($set[$i])){
502+
$nextCursorPosition = $i >= $maximumListElement ? 0 : $i + 1;
503+
504+
if ('*' === $match || 1 === preg_match($pattern, $set[$i])){
505+
$values[] = $set[$i];
506+
}
507+
508+
} else {
509+
// Out of the arrays values, return first element
510+
$nextCursorPosition = 0;
511+
}
512+
}
513+
514+
return $this->returnPipedInfo([$nextCursorPosition, $values]);
515+
}
516+
469517
// Lists
470518

471519
public function llen($key)

tests/units/RedisMock.php

+43
Original file line numberDiff line numberDiff line change
@@ -2031,6 +2031,49 @@ public function testScanCommand()
20312031
->isEqualTo([0, [0 => 'slash-key/with/slashes/1', 1 => 'slash-key/with/slashes/2']]);
20322032
}
20332033

2034+
public function testSscanCommand()
2035+
{
2036+
$redisMock = new Redis();
2037+
$redisMock->sadd('myKey', 'a1');
2038+
$redisMock->sadd('myKey', ['b1', 'b2', 'b3', 'b4', 'b5', 'b6']);
2039+
$redisMock->sadd('myKey', ['c1', 'c2', 'c3']);
2040+
$redisMock->sadd('a/b', 'c/d');
2041+
2042+
// It must return no values, as the key is unknown.
2043+
$this->assert
2044+
->array($redisMock->sscan('unknown', 1, ['COUNT' => 2]))
2045+
->isEqualTo([0, []]);
2046+
2047+
$this->assert
2048+
->array($redisMock->sscan('a/b', 0, ['MATCH' => 'c/*']))
2049+
->isEqualTo([0, [0 => 'c/d']]);
2050+
2051+
// It must return two values, start cursor after the first value of the list.
2052+
$this->assert
2053+
->array($redisMock->sscan('myKey', 1, ['COUNT' => 2]))
2054+
->isEqualTo([3, [0 => 'b1', 1 => 'b2']]);
2055+
2056+
// It must return all the values with match with the regex 'our' (2 keys).
2057+
// And the cursor is defined after the default count (10) => the match has not terminate all the list.
2058+
$this->assert
2059+
->array($redisMock->sscan('myKey', 0, ['MATCH' => 'c*']))
2060+
->isEqualTo([10, [0 => 'c1', 1 => 'c2', 2 => 'c3']]);
2061+
2062+
// Execute the match at the end of this list, the match not return an element (no one element match with the regex),
2063+
// And the list is terminate, return the cursor to the start (0)
2064+
$this->assert
2065+
->array($redisMock->sscan('myKey', 11, ['MATCH' => 'c*']))
2066+
->isEqualTo([0, []]);
2067+
2068+
$redisMock->expire('myKey', 1);
2069+
sleep(2);
2070+
2071+
// It must return no values, as the key is expired.
2072+
$this->assert
2073+
->array($redisMock->sscan('myKey', 1, ['COUNT' => 2]))
2074+
->isEqualTo([0, []]);
2075+
}
2076+
20342077
public function testBitcountCommand()
20352078
{
20362079
$redisMock = new Redis();

0 commit comments

Comments
 (0)