Skip to content

Commit

Permalink
Add ZScan Method
Browse files Browse the repository at this point in the history
  • Loading branch information
robertmarney committed Mar 26, 2023
1 parent 0027bd9 commit 39316ba
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 49 deletions.
50 changes: 21 additions & 29 deletions src/M6Web/Component/RedisMock/RedisMock.php
Original file line number Diff line number Diff line change
Expand Up @@ -1219,46 +1219,38 @@ public function zunionstore($destination, array $keys, array $options = array())
* @param array $options contain options of the command, with values (ex ['MATCH' => 'st*', 'COUNT' => 42] )
* @return $this|array|mixed
*/
public function zscan($key, $cursor = 0, array $options = [])
public function zscan($key, $cursor, $options = [])
{
$match = isset($options['MATCH']) ? $options['MATCH'] : '*';
$count = isset($options['COUNT']) ? $options['COUNT'] : 10;
$maximumValue = $cursor + $count -1;
$options = array_change_key_case($options, CASE_UPPER); // normalize to match Laravel/Symfony
$count = isset($options[ 'COUNT' ]) ? (int)$options[ 'COUNT' ] : 10;
$match = isset($options[ 'MATCH' ]) ? $options[ 'MATCH' ] : '*';
$pattern = sprintf('/^%s$/', str_replace(['*', '/'], ['.*', '\/'], $match));

$iterator = $cursor;

if (!isset(self::$dataValues[$this->storage][$key]) || $this->deleteOnTtlExpired($key)) {
return $this->returnPipedInfo([0, []]);
}

// Sorted set of all values in the storage.
$set = self::$dataValues[$this->storage][$key];
$maximumListElement = count($set);
$set = self::$dataValues[ $this->storage ][ $key ];

// Next cursor position
$nextCursorPosition = 0;
// Matched values.
$values = [];
// Pattern, for find matched values.
$pattern = sprintf('/^%s$/', str_replace(['*', '/'], ['.*', '\/'], $match));
if ($match !== '*') {
$set = array_filter($set, function($key) use ($pattern) {
return preg_match($pattern, $key);
}, ARRAY_FILTER_USE_KEY);
}

// Iterate over the sorted set starting from the given cursor position.
for($i = $cursor; $i <= $maximumValue; $i++)
{
if (isset($set[$i])){
$nextCursorPosition = $i >= $maximumListElement ? 0 : $i + 1;
$results = array_slice($set, $iterator, $count, true);
$iterator += count($results);

// Check if the score matches the pattern
$score = $set[$i][0];
if ('*' === $match || 1 === preg_match($pattern, $set[$i][1])){
$values[] = $set[$i][1];
}

} else {
// Out of the arrays values, return first element
$nextCursorPosition = 0;
}
if ($count <= count($results)) {
// there are more elements to scan
return $this->returnPipedInfo([$iterator, $results]);
} else {
// the end of the list has been reached
return $this->returnPipedInfo([0, $results]);
}

return $this->returnPipedInfo([$nextCursorPosition, $values]);
}

// Server
Expand Down
59 changes: 39 additions & 20 deletions tests/units/RedisMock.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/
class RedisMock extends atoum
{

public function testSetGetDelExists()
{
$redisMock = new Redis();
Expand Down Expand Up @@ -2077,44 +2078,62 @@ public function testSscanCommand()
public function testZscanCommand()
{
$redisMock = new Redis();
$redisMock->zadd('myZset', 1, 'a1');
$redisMock->zadd('myZset', ['b1' => 2, 'b2' => 3, 'b3' => 4, 'b4' => 5, 'b5' => 6, 'b6' => 7]);
$redisMock->zadd('myZset', ['c1' => 8, 'c2' => 9, 'c3' => 10]);
$redisMock->zadd('a/b', 11, 'c/d');
$redisMock->zadd('set1', 1, 'a:1');
$redisMock->zadd('set1', 2, 'b:1');
$redisMock->zadd('set1', 3, 'c:1');
$redisMock->zadd('set1', 4, 'd:1');

// Could be removed: ensure we have some noise of multiple sets
$redisMock->zadd('set2', 1, 'x:1');
$redisMock->zadd('set2', 2, 'y:1');
$redisMock->zadd('set2', 3, 'z:1');

// It must return no values, as the key is unknown.
$this->assert
->array($redisMock->zscan('unknown', 1, ['COUNT' => 2]))
->array($redisMock->zscan('unknown', 0, ['COUNT' => 10]))
->isEqualTo([0, []]);


// It must return all the values with score greater than or equal to 1.
$this->assert
->array($redisMock->zscan('a/b', 0, ['MATCH' => 'c/*']))
->isEqualTo([0, [0 => 'c/d']]);
->array($redisMock->zscan('set1', 0, ['MATCH' => '*', 'COUNT' => 10]))
->isEqualTo([0 => 0, 1 => ['a:1' => 1, 'b:1' => 2, 'c:1' => 3, 'd:1' => 4]]);

// It must return two values, start cursor after the first value of the set.
// It must return only the matched value
$this->assert
->array($redisMock->zscan('myZset', 1, ['COUNT' => 2]))
->isEqualTo([3, [0 => 'b1', 1 => 'b2']]);
->array($redisMock->zscan('set1', 0, ['MATCH' => 'c*', 'COUNT' => 10]))
->isEqualTo([0 => 0, 1 => ['c:1' => 3]]);

// It must return all of the values based on the match of *1
$this->assert
->array($redisMock->zscan('set1', 0, ['MATCH' => '*1', 'COUNT' => 10]))
->isEqualTo([0 => 0, 1 => ['a:1' => 1, 'b:1' => 2, 'c:1' => 3, 'd:1' => 4]]);

// It must return two values, starting cursor after the first value of the list.

// It must return all the values with score greater than or equal to 8.
// And the cursor is defined after the default count (10) => the match has not terminate all the set.
$this->assert
->array($redisMock->zscan('myZset', 0, ['MATCH' => '*', 'COUNT' => 10]))
->isEqualTo([10, [0 => 'a1', 1 => 'b1', 2 => 'b2', 3 => 'b3', 4 => 'b4', 5 => 'b5', 6 => 'b6', 7 => 'c1', 8 => 'c2', 9 => 'c3']]);
->array($redisMock->zscan('set1', 1, ['COUNT' => 2]))
->isEqualTo([3, ['b:1' => 2, 'c:1' => 3]]);

// Execute the match at the end of this set, the match not return an element (no one element match with the regex),
// And the set is terminate, return the cursor to the start (0)
// Ensure if our results are complete we return a zero cursor
$this->assert
->array($redisMock->zscan('myZset', 11, ['MATCH' => 'd*']))
->isEqualTo([0, []]);
->array($redisMock->zscan('set1', 3, ['COUNT' => 2]))
->isEqualTo([0, ['d:1' => 4]]);

$redisMock->expire('myZset', 1);
// It must return all the values with score greater than or equal to 3,
// starting cursor after the last value of the previous scan.
$this->assert
->array($redisMock->zscan('set1', 4, ['MATCH' => '*', 'COUNT' => 10]))
->isEqualTo([0 => 0, 1 => []]);

$redisMock->expire('set1', 1);
sleep(2);

// It must return no values, as the key is expired.
$this->assert
->array($redisMock->zscan('myZset', 1, ['COUNT' => 2]))
->array($redisMock->zscan('set1', 0, ['COUNT' => 2]))
->isEqualTo([0, []]);

}

public function testBitcountCommand()
Expand Down

0 comments on commit 39316ba

Please sign in to comment.