Extended Geospatial support by implementing GEOSEARCHSTORE command (#873)

* Added arrayable arguments classes

* Updated count trait to accept additional modifier argument

* Added new With traits, moved into separate directory

* Added traits for geo command arguments resolving

* Remove BaseWith trait, not working with traits nesting

* Added AscDesc trait

* Rename AscDesc traint into Sorting

* Fixed trait keyword

* Removed unnecessary traits

* Updated count trait to handle default argument value

* Fixed With traits offset check condition

* Added GeoFrom, GeoBy traits

* Fixes for Count and Sorting traits

* Fixed variable names in with traits

* [WIP] Added GEOSEARCH command support, without test coverage

* Changed functionality to support only lower case units (Redis 6.0 support)

* Changed namespace for WITHSCORES trait

* Added more test coverage, added response parsing

* Added support for GEOSEARCHSTORE command

* Added test coverage for tratis

Co-authored-by: Vladyslav Vildanov <vladyslavvildanov@Vladyslav-Vildanov-MacBook-Pro.local>
Co-authored-by: Chayim <chayim@users.noreply.github.com>
This commit is contained in:
Vladyslav Vildanov
2022-12-29 22:16:51 +02:00
committed by GitHub
parent bb65b31580
commit b47aa7ad3a
8 changed files with 548 additions and 0 deletions
+1
View File
@@ -185,6 +185,7 @@ use Predis\Command\CommandInterface;
* @method $this georadius($key, $longitude, $latitude, $radius, $unit, array $options = null)
* @method $this georadiusbymember($key, $member, $radius, $unit, array $options = null)
* @method $this geosearch(string $key, FromInterface $from, ByInterface $by, ?string $sorting = null, int $count = -1, bool $any = false, bool $withCoord = false, bool $withDist = false, bool $withHash = false)
* @method $this geosearchstore(string $destination, string $source, FromInterface $from, ByInterface $by, ?string $sorting = null, int $count = -1, bool $any = false, bool $storeDist = false)
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
+1
View File
@@ -203,6 +203,7 @@ use Predis\Response\Status;
* @method array georadius(string $key, $longitude, $latitude, $radius, $unit, array $options = null)
* @method array georadiusbymember(string $key, $member, $radius, $unit, array $options = null)
* @method array geosearch(string $key, FromInterface $from, ByInterface $by, ?string $sorting = null, int $count = -1, bool $any = false, bool $withCoord = false, bool $withDist = false, bool $withHash = false)
* @method int geosearchstore(string $destination, string $source, FromInterface $from, ByInterface $by, ?string $sorting = null, int $count = -1, bool $any = false, bool $storeDist = false)
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
+5
View File
@@ -14,6 +14,11 @@ namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
/**
* @deprecated As of Redis version 6.2.0, this command is regarded as deprecated.
*
* It can be replaced by GEOSEARCH and GEOSEARCHSTORE with the BYRADIUS argument
* when migrating or writing new code.
*
* @link http://redis.io/commands/georadius
*
* @author Daniele Alessandri <suppakilla@gmail.com>
+5
View File
@@ -12,6 +12,11 @@
namespace Predis\Command\Redis;
/**
* @deprecated As of Redis version 6.2.0, this command is regarded as deprecated.
*
* It can be replaced by GEOSEARCH and GEOSEARCHSTORE with the FROMMEMBER arguments
* when migrating or writing new code.
*
* @link http://redis.io/commands/georadiusbymember
*
* @author Daniele Alessandri <suppakilla@gmail.com>
+61
View File
@@ -0,0 +1,61 @@
<?php
namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
use Predis\Command\Traits\By\GeoBy;
use Predis\Command\Traits\Count;
use Predis\Command\Traits\From\GeoFrom;
use Predis\Command\Traits\Sorting;
use Predis\Command\Traits\Storedist;
/**
* @link https://redis.io/commands/geosearchstore/
*
* This command is like GEOSEARCH, but stores the result in destination key.
*/
class GEOSEARCHSTORE extends RedisCommand
{
use GeoFrom {
GeoFrom::setArguments as setFrom;
}
use GeoBy {
GeoBy::setArguments as setBy;
}
use Sorting {
Sorting::setArguments as setSorting;
}
use Count {
Count::setArguments as setCount;
}
use Storedist {
Storedist::setArguments as setStoreDist;
}
protected static $sortArgumentPositionOffset = 4;
protected static $countArgumentPositionOffset = 5;
protected static $storeDistArgumentPositionOffset = 7;
public function getId()
{
return 'GEOSEARCHSTORE';
}
public function setArguments(array $arguments)
{
$this->setStoreDist($arguments);
$arguments = $this->getArguments();
$this->setCount($arguments, $arguments[6] ?? false);
$arguments = $this->getArguments();
$this->setSorting($arguments);
$arguments = $this->getArguments();
$this->setFrom($arguments);
$arguments = $this->getArguments();
$this->setBy($arguments);
$this->filterArguments();
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace Predis\Command\Traits;
use Predis\Command\Command;
use UnexpectedValueException;
/**
* @mixin Command
*/
trait Storedist
{
public function setArguments(array $arguments)
{
$argumentsLength = count($arguments);
if (
static::$storeDistArgumentPositionOffset >= $argumentsLength
|| false === $arguments[static::$storeDistArgumentPositionOffset]
) {
parent::setArguments($arguments);
return;
}
$argument = $arguments[static::$storeDistArgumentPositionOffset];
if (true === $argument) {
$argument = 'STOREDIST';
} else {
throw new UnexpectedValueException("Wrong STOREDIST argument type");
}
$argumentsBefore = array_slice($arguments, 0, static::$storeDistArgumentPositionOffset);
$argumentsAfter = array_slice($arguments, static::$storeDistArgumentPositionOffset + 1);
parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter));
}
}
@@ -0,0 +1,354 @@
<?php
namespace Predis\Command\Redis;
use InvalidArgumentException;
use Predis\Command\Argument\Geospatial\ByBox;
use Predis\Command\Argument\Geospatial\ByInterface;
use Predis\Command\Argument\Geospatial\ByRadius;
use Predis\Command\Argument\Geospatial\FromInterface;
use Predis\Command\Argument\Geospatial\FromLonLat;
use Predis\Command\Argument\Geospatial\FromMember;
use UnexpectedValueException;
class GEOSEARCHSTORE_Test extends PredisCommandTestCase
{
/**
* @inheritDoc
*/
protected function getExpectedCommand(): string
{
return GEOSEARCHSTORE::class;
}
/**
* @inheritDoc
*/
protected function getExpectedId(): string
{
return 'GEOSEARCHSTORE';
}
/**
* @group disconnected
* @dataProvider argumentsProvider
*/
public function testFilterArguments(array $actualArguments, array $expectedArguments): void
{
$command = $this->getCommand();
$command->setArguments($actualArguments);
$this->assertSameValues($expectedArguments, $command->getArguments());
}
/**
* @group disconnected
*/
public function testParseResponse(): void
{
$this->assertSame(1, $this->getCommand()->parseResponse(1));
}
/**
* @group connected
* @dataProvider coordinatesProvider
* @param array $firstCoordinates
* @param array $secondCoordinates
* @param array $thirdCoordinates
* @param string $destination
* @param string $source
* @param FromInterface $from
* @param ByInterface $by
* @param string|null $sorting
* @param int $count
* @param bool $any
* @param int $expectedResultingElements
* @param array $expectedResponse
* @return void
* @requiresRedisVersion >= 6.2.0
*/
public function testStoresCorrectGivenGeospatialCoordinates(
array $firstCoordinates,
array $secondCoordinates,
array $thirdCoordinates,
string $destination,
string $source,
FromInterface $from,
ByInterface $by,
?string $sorting,
int $count,
bool $any,
int $expectedResultingElements,
array $expectedResponse
): void {
$redis = $this->getClient();
$redis->geoadd(...$firstCoordinates);
$redis->geoadd(...$secondCoordinates);
$redis->geoadd(...$thirdCoordinates);
$actualResultingElements = $redis->geosearchstore(
$destination,
$source,
$from,
$by,
$sorting,
$count,
$any
);
$this->assertSame($expectedResultingElements, $actualResultingElements);
$this->assertSame($expectedResponse, $redis->geosearch($destination, $from, $by, $sorting, $count, $any));
}
/**
* @group connected
* @return void
* @requiresRedisVersion >= 6.2.0
*/
public function testStoresInSortedSetWithStoreDistArgumentProvided(): void
{
$redis = $this->getClient();
$redis->geoadd('key', 1.1, 2, 'member1');
$redis->geoadd('key', 2.1, 3, 'member2');
$redis->geoadd('key', 3.1, 4, 'member3');
$actualResultingElements = $redis->geosearchstore(
'destination',
'key',
new FromLonLat(1, 4),
new ByRadius(9999, 'km'),
null,
2,
false,
true
);
$this->assertSame(2, $actualResultingElements);
$this->assertSame(['member2', 'member1'], $redis->zrange('destination', 0, -1));
}
/**
* @group connected
* @dataProvider unexpectedValuesProvider
* @param array $arguments
* @param string $expectedException
* @param string $expectedExceptionMessage
* @return void
* @requiresRedisVersion >= 6.2.0
*/
public function testThrowsExceptionOnUnexpectedValueProvided(
array $arguments,
string $expectedException,
string $expectedExceptionMessage
): void {
$redis = $this->getClient();
$this->expectException($expectedException);
$this->expectExceptionMessage($expectedExceptionMessage);
$redis->geosearchstore(...$arguments);
}
public function argumentsProvider(): array
{
return [
'with default arguments - FROMLONLAT, BYRADIUS' => [
['destination', 'source', new FromLonLat(1.1, 2.2), new ByRadius(1, 'km')],
['destination', 'source', 'FROMLONLAT', 1.1, 2.2, 'BYRADIUS', 1, 'km']
],
'with default arguments - FROMMEMBER, BYBOX' => [
['destination', 'source', new FromMember('member'), new ByBox(1,1, 'km')],
['destination', 'source', 'FROMMEMBER', 'member', 'BYBOX', 1, 1, 'km']
],
'with ASC sorting' => [
['destination', 'source', new FromLonLat(1.1, 2.2), new ByRadius(1, 'km'), 'asc'],
['destination', 'source', 'FROMLONLAT', 1.1, 2.2, 'BYRADIUS', 1, 'km', 'ASC']
],
'with DESC sorting' => [
['destination', 'source', new FromLonLat(1.1, 2.2), new ByRadius(1, 'km'), 'desc'],
['destination', 'source', 'FROMLONLAT', 1.1, 2.2, 'BYRADIUS', 1, 'km', 'DESC']
],
'with COUNT argument - without ANY option' => [
['destination', 'source', new FromLonLat(1.1, 2.2), new ByRadius(1, 'km'), null, 20],
['destination', 'source', 'FROMLONLAT', 1.1, 2.2, 'BYRADIUS', 1, 'km', 'COUNT', 20]
],
'with COUNT argument - with ANY option' => [
['destination', 'source', new FromLonLat(1.1, 2.2), new ByRadius(1, 'km'), null, 20, true],
['destination', 'source', 'FROMLONLAT', 1.1, 2.2, 'BYRADIUS', 1, 'km', 'COUNT', 20, 'ANY']
],
'with STOREDIST argument' => [
['destination', 'source', new FromLonLat(1.1, 2.2), new ByRadius(1, 'km'), null, -1, false, true],
['destination', 'source', 'FROMLONLAT', 1.1, 2.2, 'BYRADIUS', 1, 'km', 'STOREDIST']
],
'with all arguments' => [
['destination', 'source', new FromLonLat(1.1, 2.2), new ByRadius(1, 'km'), 'asc', 20, true, true],
['destination', 'source', 'FROMLONLAT', 1.1, 2.2, 'BYRADIUS', 1, 'km', 'ASC', 'COUNT', 20, 'ANY', 'STOREDIST']
]
];
}
public function coordinatesProvider(): array
{
return [
'with default arguments - FROMLONLAT, BYRADIUS - all members' => [
['key', 1.1, 2, 'member1'],
['key', 2.1, 3, 'member2'],
['key', 3.1, 4, 'member3'],
'destination',
'key',
new FromLonLat(1, 4),
new ByRadius(9999, 'km'),
null,
-1,
false,
3,
['member1', 'member2', 'member3']
],
'with default arguments - FROMLONLAT, BYRADIUS - closest members' => [
['key', 1.1, 2, 'member1'],
['key', 2.1, 3, 'member2'],
['key', 3.1, 4, 'member3'],
'destination',
'key',
new FromLonLat(1, 3),
new ByRadius(200, 'km'),
null,
-1,
false,
2,
['member2', 'member1']
],
'with default arguments - FROMMEMBER, BYBOX - all members' => [
['key', 1.1, 2, 'member1'],
['key', 2.1, 3, 'member2'],
['key', 3.1, 4, 'member3'],
'destination',
'key',
new FromMember('member2'),
new ByBox(999, 999, 'km'),
null,
-1,
false,
3,
['member1', 'member2', 'member3']
],
'with default arguments - FROMMEMBER, BYBOX - closest members' => [
['key', 1.1, 2, 'member1'],
['key', 2.1, 3, 'member2'],
['key', 3.1, 4, 'member3'],
'destination',
'key',
new FromMember('member1'),
new ByBox(300, 300, 'km'),
null,
-1,
false,
2,
['member1', 'member2']
],
'with ASC modifier' => [
['key', 1.1, 2, 'member1'],
['key', 2.1, 3, 'member2'],
['key', 3.1, 4, 'member3'],
'destination',
'key',
new FromLonLat(1, 4),
new ByRadius(9999, 'km'),
'asc',
-1,
false,
3,
['member2', 'member1', 'member3']
],
'with DESC modifier' => [
['key', 1.1, 2, 'member1'],
['key', 2.1, 3, 'member2'],
['key', 3.1, 4, 'member3'],
'destination',
'key',
new FromLonLat(1, 4),
new ByRadius(9999, 'km'),
'desc',
-1,
false,
3,
['member3', 'member1', 'member2']
],
'with COUNT modifier - without ANY option' => [
['key', 1.1, 2, 'member1'],
['key', 2.1, 3, 'member2'],
['key', 3.1, 4, 'member3'],
'destination',
'key',
new FromLonLat(1, 4),
new ByRadius(9999, 'km'),
null,
1,
false,
1,
['member2']
],
'with COUNT modifier - with ANY option' => [
['key', 1.1, 2, 'member1'],
['key', 2.1, 3, 'member2'],
['key', 3.1, 4, 'member3'],
'destination',
'key',
new FromLonLat(1, 4),
new ByRadius(9999, 'km'),
null,
2,
true,
2,
['member1', 'member2']
],
'with all arguments' => [
['key', 1.1, 2, 'member1'],
['key', 2.1, 3, 'member2'],
['key', 3.1, 4, 'member3'],
'destination',
'key',
new FromLonLat(1, 4),
new ByRadius(9999, 'km'),
'asc',
2,
true,
2,
['member2', 'member1']
],
];
}
public function unexpectedValuesProvider(): array
{
return [
'with wrong FROM argument' => [
['destination', 'source', false, new ByRadius(9999, 'km'), null, -1, false, false],
InvalidArgumentException::class,
'Invalid FROM argument value given'
],
'with wrong BY argument' => [
['destination', 'source', new FromLonLat(1, 4), false, null, -1, false, false],
InvalidArgumentException::class,
'Invalid BY argument value given'
],
'with wrong sorting argument' => [
['destination', 'source', new FromLonLat(1, 4), new ByRadius(9999, 'km'), 'wrong', -1, false, false],
UnexpectedValueException::class,
'Sorting argument accepts only: asc, desc values'
],
'with wrong COUNT argument' => [
['destination', 'source', new FromLonLat(1, 4), new ByRadius(9999, 'km'), null, 0, false, false],
UnexpectedValueException::class,
'Wrong count argument value or position offset'
],
'with wrong STOREDIST argument' => [
['destination', 'source', new FromLonLat(1, 4), new ByRadius(9999, 'km'), null, 0, false, 'wrong'],
UnexpectedValueException::class,
'Wrong STOREDIST argument type'
],
];
}
}
@@ -0,0 +1,83 @@
<?php
namespace Predis\Command\Traits;
use Predis\Command\Command as RedisCommand;
use PredisTestCase;
use UnexpectedValueException;
class StoredistTest extends PredisTestCase
{
private $testClass;
protected function setUp(): void
{
parent::setUp();
$this->testClass = new class extends RedisCommand {
use Storedist;
public static $storeDistArgumentPositionOffset = 0;
public function getId()
{
return 'test';
}
};
}
/**
* @dataProvider argumentsProvider
* @param int $offset
* @param array $actualArguments
* @param array $expectedArguments
* @return void
*/
public function testReturnsCorrectArguments(int $offset, array $actualArguments, array $expectedArguments): void
{
$this->testClass::$storeDistArgumentPositionOffset = $offset;
$this->testClass->setArguments($actualArguments);
$this->assertSame($expectedArguments, $this->testClass->getArguments());
}
/**
* @return void
*/
public function testThrowsExceptionOnUnexpectedValue(): void
{
$this->testClass::$storeDistArgumentPositionOffset = 0;
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage("Wrong STOREDIST argument type");
$this->testClass->setArguments(['test']);
}
public function argumentsProvider(): array
{
return [
'STOREDIST false argument' => [
0,
[false, 'second argument', 'third argument'],
[false, 'second argument', 'third argument']
],
'STOREDIST argument first and there is arguments after' => [
0,
[true, 'second argument', 'third argument'],
['STOREDIST', 'second argument', 'third argument']
],
'STOREDIST argument last and there is arguments before' => [
2,
['first argument', 'second argument', true],
['first argument', 'second argument', 'STOREDIST']
],
'STOREDIST argument not the first and not the last' => [
1,
['first argument', true, 'third argument'],
['first argument', 'STOREDIST', 'third argument']
],
];
}
}