Przeglądaj źródła

Add new command: GEORADIUS (Redis 3.2.0).

Daniele Alessandri 9 lat temu
rodzic
commit
00000fde4e

+ 1 - 0
src/ClientContextInterface.php

@@ -160,6 +160,7 @@ use Predis\Command\CommandInterface;
  * @method $this geohash($key, array $members)
  * @method $this geohash($key, array $members)
  * @method $this geopos($key, array $members)
  * @method $this geopos($key, array $members)
  * @method $this geodist($key, $member1, $member2, $unit = null)
  * @method $this geodist($key, $member1, $member2, $unit = null)
+ * @method $this georadius($key, $longitude, $latitude, $radius, $unit, array $options = null)
  *
  *
  * @author Daniele Alessandri <suppakilla@gmail.com>
  * @author Daniele Alessandri <suppakilla@gmail.com>
  */
  */

+ 1 - 0
src/ClientInterface.php

@@ -168,6 +168,7 @@ use Predis\Profile\ProfileInterface;
  * @method array  geohash($key, array $members)
  * @method array  geohash($key, array $members)
  * @method array  geopos($key, array $members)
  * @method array  geopos($key, array $members)
  * @method string geodist($key, $member1, $member2, $unit = null)
  * @method string geodist($key, $member1, $member2, $unit = null)
+ * @method array  georadius($key, $longitude, $latitude, $radius, $unit, array $options = null)
  *
  *
  * @author Daniele Alessandri <suppakilla@gmail.com>
  * @author Daniele Alessandri <suppakilla@gmail.com>
  */
  */

+ 33 - 0
src/Cluster/ClusterStrategy.php

@@ -171,6 +171,7 @@ abstract class ClusterStrategy implements StrategyInterface
             'GEOHASH' => $getKeyFromFirstArgument,
             'GEOHASH' => $getKeyFromFirstArgument,
             'GEOPOS' => $getKeyFromFirstArgument,
             'GEOPOS' => $getKeyFromFirstArgument,
             'GEODIST' => $getKeyFromFirstArgument,
             'GEODIST' => $getKeyFromFirstArgument,
+            'GEORADIUS' => array($this, 'getKeyFromGeoradiusCommands'),
         );
         );
     }
     }
 
 
@@ -300,6 +301,38 @@ abstract class ClusterStrategy implements StrategyInterface
         }
         }
     }
     }
 
 
+    /**
+     * Extracts the key from GEORADIUS command.
+     *
+     * @param CommandInterface $command Command instance.
+     *
+     * @return string|null
+     */
+    protected function getKeyFromGeoradiusCommands(CommandInterface $command)
+    {
+        $arguments = $command->getArguments();
+        $argc = count($arguments);
+
+        if ($argc > 5) {
+            $keys = array($arguments[0]);
+
+            for ($i = 5; $i < $argc; $i++) {
+                $argument = strtoupper($arguments[$i]);
+                if ($argument === 'STORE' || $argument === 'STOREDIST') {
+                    $keys[] = $arguments[++$i];
+                }
+            }
+
+            if ($this->checkSameSlotForKeys($keys)) {
+                return $arguments[0];
+            } else {
+                return null;
+            }
+        }
+
+        return $arguments[0];
+    }
+
     /**
     /**
      * Extracts the key from ZINTERSTORE and ZUNIONSTORE commands.
      * Extracts the key from ZINTERSTORE and ZUNIONSTORE commands.
      *
      *

+ 71 - 0
src/Command/GeospatialGeoRadius.php

@@ -0,0 +1,71 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command;
+
+/**
+ * @link http://redis.io/commands/georadius
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ */
+class GeospatialGeoRadius extends Command
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getId()
+    {
+        return 'GEORADIUS';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function filterArguments(array $arguments)
+    {
+        if ($arguments && is_array(end($arguments))) {
+            $options = array_change_key_case(array_pop($arguments), CASE_UPPER);
+
+            if (isset($options['WITHCOORD']) && $options['WITHCOORD'] == true) {
+                $arguments[] = 'WITHCOORD';
+            }
+
+            if (isset($options['WITHDIST']) && $options['WITHDIST'] == true) {
+                $arguments[] = 'WITHDIST';
+            }
+
+            if (isset($options['WITHHASH']) && $options['WITHHASH'] == true) {
+                $arguments[] = 'WITHHASH';
+            }
+
+            if (isset($options['COUNT'])) {
+                $arguments[] = 'COUNT';
+                $arguments[] = $options['COUNT'];
+            }
+
+            if (isset($options['SORT'])) {
+                $arguments[] = strtoupper($options['SORT']);
+            }
+
+            if (isset($options['STORE'])) {
+                $arguments[] = 'STORE';
+                $arguments[] = $options['STORE'];
+            }
+
+            if (isset($options['STOREDIST'])) {
+                $arguments[] = 'STOREDIST';
+                $arguments[] = $options['STOREDIST'];
+            }
+        }
+
+        return $arguments;
+    }
+}

+ 28 - 0
src/Command/Processor/KeyPrefixProcessor.php

@@ -164,6 +164,7 @@ class KeyPrefixProcessor implements ProcessorInterface
             'GEOHASH' => 'static::first',
             'GEOHASH' => 'static::first',
             'GEOPOS' => 'static::first',
             'GEOPOS' => 'static::first',
             'GEODIST' => 'static::first',
             'GEODIST' => 'static::first',
+            'GEORADIUS' => 'static::georadius',
         );
         );
     }
     }
 
 
@@ -417,4 +418,31 @@ class KeyPrefixProcessor implements ProcessorInterface
             $command->setRawArguments($arguments);
             $command->setRawArguments($arguments);
         }
         }
     }
     }
+
+    /**
+     * Applies the specified prefix to the key of a GEORADIUS command.
+     *
+     * @param CommandInterface $command Command instance.
+     * @param string           $prefix  Prefix string.
+     */
+    public static function georadius(CommandInterface $command, $prefix)
+    {
+        if ($arguments = $command->getArguments()) {
+            $arguments[0] = "$prefix{$arguments[0]}";
+
+            if (($count = count($arguments)) > 5) {
+                for ($i = 5; $i < $count; ++$i) {
+                    switch (strtoupper($arguments[$i])) {
+                        case 'STORE':
+                        case 'STOREDIST':
+                            $arguments[$i] = "$prefix{$arguments[++$i]}";
+                            break;
+
+                    }
+                }
+            }
+
+            $command->setRawArguments($arguments);
+        }
+    }
 }
 }

+ 1 - 0
src/Profile/RedisVersion320.php

@@ -274,6 +274,7 @@ class RedisVersion320 extends RedisProfile
             'GEOHASH' => 'Predis\Command\GeospatialGeoHash',
             'GEOHASH' => 'Predis\Command\GeospatialGeoHash',
             'GEOPOS' => 'Predis\Command\GeospatialGeoPos',
             'GEOPOS' => 'Predis\Command\GeospatialGeoPos',
             'GEODIST' => 'Predis\Command\GeospatialGeoDist',
             'GEODIST' => 'Predis\Command\GeospatialGeoDist',
+            'GEORADIUS' => 'Predis\Command\GeospatialGeoRadius',
         );
         );
     }
     }
 }
 }

+ 26 - 0
src/Replication/ReplicationStrategy.php

@@ -129,6 +129,31 @@ class ReplicationStrategy
         return true;
         return true;
     }
     }
 
 
+    /**
+     * Checks if a GEORADIUS command is a readable operation by parsing the
+     * arguments array of the specified commad instance.
+     *
+     * @param CommandInterface $command Command instance.
+     *
+     * @return bool
+     */
+    protected function isGeoradiusReadOnly(CommandInterface $command)
+    {
+        $arguments = $command->getArguments();
+        $argc = count($arguments);
+
+        if ($argc > 5) {
+            for ($i = 5; $i < $argc; $i++) {
+                $argument = strtoupper($arguments[$i]);
+                if ($argument === 'STORE' || $argument === 'STOREDIST') {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
     /**
     /**
      * Marks a command as a read-only operation.
      * Marks a command as a read-only operation.
      *
      *
@@ -261,6 +286,7 @@ class ReplicationStrategy
             'GEOHASH' => true,
             'GEOHASH' => true,
             'GEOPOS' => true,
             'GEOPOS' => true,
             'GEODIST' => true,
             'GEODIST' => true,
+            'GEORADIUS' => array($this, 'isGeoradiusReadOnly'),
         );
         );
     }
     }
 }
 }

+ 18 - 0
tests/Predis/Cluster/PredisStrategyTest.php

@@ -152,6 +152,23 @@ class PredisStrategyTest extends PredisTestCase
         }
         }
     }
     }
 
 
+    /**
+     * @group disconnected
+     */
+    public function testKeysForGeoradiusCommand()
+    {
+        $strategy = $this->getClusterStrategy();
+        $profile = Profile\Factory::getDevelopment();
+
+        $commandID = 'GEORADIUS';
+
+        $command = $profile->createCommand($commandID, array('{key}:1', 10, 10, 1, 'km'));
+        $this->assertNotNull($strategy->getSlot($command), $commandID);
+
+        $command = $profile->createCommand($commandID, array('{key}:1', 10, 10, 1, 'km', 'store', '{key}:2', 'storedist', '{key}:3'));
+        $this->assertNotNull($strategy->getSlot($command), $commandID);
+    }
+
     /**
     /**
      * @group disconnected
      * @group disconnected
      */
      */
@@ -383,6 +400,7 @@ class PredisStrategyTest extends PredisTestCase
             'GEOHASH' => 'keys-first',
             'GEOHASH' => 'keys-first',
             'GEOPOS' => 'keys-first',
             'GEOPOS' => 'keys-first',
             'GEODIST' => 'keys-first',
             'GEODIST' => 'keys-first',
+            'GEORADIUS' => 'keys-georadius',
         );
         );
 
 
         if (isset($type)) {
         if (isset($type)) {

+ 18 - 0
tests/Predis/Cluster/RedisStrategyTest.php

@@ -165,6 +165,23 @@ class RedisStrategyTest extends PredisTestCase
         }
         }
     }
     }
 
 
+    /**
+     * @group disconnected
+     */
+    public function testKeysForGeoradiusCommand()
+    {
+        $strategy = $this->getClusterStrategy();
+        $profile = Profile\Factory::getDevelopment();
+
+        $commandID = 'GEORADIUS';
+
+        $command = $profile->createCommand($commandID, array('{key}:1', 10, 10, 1, 'km'));
+        $this->assertNotNull($strategy->getSlot($command), $commandID);
+
+        $command = $profile->createCommand($commandID, array('{key}:1', 10, 10, 1, 'km', 'store', '{key}:2', 'storedist', '{key}:3'));
+        $this->assertNotNull($strategy->getSlot($command), $commandID);
+    }
+
     /**
     /**
      * @group disconnected
      * @group disconnected
      */
      */
@@ -393,6 +410,7 @@ class RedisStrategyTest extends PredisTestCase
             'GEOHASH' => 'keys-first',
             'GEOHASH' => 'keys-first',
             'GEOPOS' => 'keys-first',
             'GEOPOS' => 'keys-first',
             'GEODIST' => 'keys-first',
             'GEODIST' => 'keys-first',
+            'GEORADIUS' => 'keys-georadius',
         );
         );
 
 
         if (isset($type)) {
         if (isset($type)) {

+ 170 - 0
tests/Predis/Command/GeospatialGeoRadiusTest.php

@@ -0,0 +1,170 @@
+<?php
+
+/*
+ * This file is part of the Predis package.
+ *
+ * (c) Daniele Alessandri <suppakilla@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Predis\Command;
+
+/**
+ * @group commands
+ * @group realm-geospatial
+ */
+class GeospatialGeoRadiusTest extends PredisCommandTestCase
+{
+    /**
+     * {@inheritdoc}
+     */
+    protected function getExpectedCommand()
+    {
+        return 'Predis\Command\GeospatialGeoRadius';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function getExpectedId()
+    {
+        return 'GEORADIUS';
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testFilterArguments()
+    {
+        $arguments = array(
+            'Sicily', 15, 37, 200, 'km',
+            'WITHCOORD', 'WITHDIST', 'WITHHASH', 'COUNT', 1, 'ASC', 'STORE', 'key:store', 'STOREDIST', 'key:storedist'
+        );
+
+        $expected = array(
+            'Sicily', 15, 37, 200, 'km',
+            'WITHCOORD', 'WITHDIST', 'WITHHASH', 'COUNT', 1, 'ASC', 'STORE', 'key:store', 'STOREDIST', 'key:storedist'
+        );
+
+        $command = $this->getCommand();
+        $command->setArguments($arguments);
+
+        $this->assertSame($expected, $command->getArguments());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testFilterArgumentsWithComplexOptions()
+    {
+        $arguments = array(
+            'Sicily', 15, 37, 200, 'km', array(
+                'store' => 'key:store',
+                'storedist' => 'key:storedist',
+                'withdist' => true,
+                'withcoord' => true,
+                'withhash' => true,
+                'count' => 1,
+                'sort' => 'asc',
+            ),
+        );
+
+        $expected = array(
+            'Sicily', 15, 37, 200, 'km',
+            'WITHCOORD', 'WITHDIST', 'WITHHASH', 'COUNT', 1, 'ASC', 'STORE', 'key:store', 'STOREDIST', 'key:storedist'
+        );
+
+        $command = $this->getCommand();
+        $command->setArguments($arguments);
+
+        $this->assertSame($expected, $command->getArguments());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testFilterArgumentsWithSpecificOptionsSetToFalse()
+    {
+        $arguments = array(
+            'Sicily', 15, 37, 200, 'km', array(
+                'store' => 'key:store',
+                'storedist' => 'key:storedist',
+                'withdist' => false,
+                'withcoord' => false,
+                'withhash' => false,
+                'count' => 1,
+                'sort' => 'asc',
+            ),
+        );
+
+        $expected = array('Sicily', 15, 37, 200, 'km', 'COUNT', 1, 'ASC', 'STORE', 'key:store', 'STOREDIST', 'key:storedist');
+
+        $command = $this->getCommand();
+        $command->setArguments($arguments);
+
+        $this->assertSame($expected, $command->getArguments());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testParseResponseWithNoOptions()
+    {
+        $raw = array(
+            array('Palermo', '190.4424'),
+            array('Catania', '56.4413'),
+        );
+
+        $expected = array(
+            array('Palermo', '190.4424'),
+            array('Catania', '56.4413'),
+        );
+
+        $command = $this->getCommand();
+
+        $this->assertSame($expected, $command->parseResponse($raw));
+    }
+
+    /**
+     * @group connected
+     * @requiresRedisVersion >= 3.2.0
+     */
+    public function testCommandReturnsGeoRadiusInfoWithNoOptions()
+    {
+        $redis = $this->getClient();
+
+        $redis->geoadd('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania');
+        $this->assertEquals(array('Palermo', 'Catania'), $redis->georadius('Sicily', 15, 37, 200, 'km'));
+    }
+
+    /**
+     * @group connected
+     * @requiresRedisVersion >= 3.2.0
+     */
+    public function testCommandReturnsGeoRadiusInfoWithOptions()
+    {
+        $redis = $this->getClient();
+
+        $redis->geoadd('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania');
+        $this->assertEquals(array(
+            array('Palermo', '190.4424', array('13.361389338970184', '38.115556395496299')),
+            array('Catania', '56.4413', array('15.087267458438873', '37.50266842333162')),
+        ), $redis->georadius('Sicily', 15, 37, 200, 'km', 'WITHDIST', 'WITHCOORD'));
+    }
+
+    /**
+     * @group connected
+     * @requiresRedisVersion >= 3.2.0
+     * @expectedException \Predis\Response\ServerException
+     * @expectedExceptionMessage Operation against a key holding the wrong kind of value
+     */
+    public function testThrowsExceptionOnWrongType()
+    {
+        $redis = $this->getClient();
+
+        $redis->lpush('Sicily', 'Palermo');
+        $redis->georadius('Sicily', 15, 37, 200, 'km');
+    }
+}

+ 8 - 0
tests/Predis/Command/Processor/KeyPrefixProcessorTest.php

@@ -889,6 +889,14 @@ class KeyPrefixProcessorTest extends PredisTestCase
                 array('key', 'member:1', 'member:2', 'km'),
                 array('key', 'member:1', 'member:2', 'km'),
                 array('prefix:key', 'member:1', 'member:2', 'km'),
                 array('prefix:key', 'member:1', 'member:2', 'km'),
             ),
             ),
+            array('GEORADIUS',
+                array('key', '15', '37', '200', 'km'),
+                array('prefix:key', '15', '37', '200', 'km'),
+            ),
+            array('GEORADIUS',
+                array('key', '15', '37', '200', 'km', 'WITHDIST', 'STORE', 'key:store', 'STOREDIST', 'key:storedist'),
+                array('prefix:key', '15', '37', '200', 'km', 'WITHDIST', 'STORE', 'prefix:key:store', 'STOREDIST', 'prefix:key:storedist'),
+            ),
         );
         );
     }
     }
 }
 }

+ 1 - 0
tests/Predis/Profile/RedisUnstableTest.php

@@ -195,6 +195,7 @@ class RedisUnstableTest extends PredisProfileTestCase
             154 => 'GEOHASH',
             154 => 'GEOHASH',
             155 => 'GEOPOS',
             155 => 'GEOPOS',
             156 => 'GEODIST',
             156 => 'GEODIST',
+            157 => 'GEORADIUS',
         );
         );
     }
     }
 }
 }

+ 1 - 0
tests/Predis/Profile/RedisVersion320Test.php

@@ -195,6 +195,7 @@ class RedisVersion320Test extends PredisProfileTestCase
             154 => 'GEOHASH',
             154 => 'GEOHASH',
             155 => 'GEOPOS',
             155 => 'GEOPOS',
             156 => 'GEODIST',
             156 => 'GEODIST',
+            157 => 'GEORADIUS',
         );
         );
     }
     }
 }
 }

+ 28 - 0
tests/Predis/Replication/ReplicationStrategyTest.php

@@ -139,6 +139,33 @@ class ReplicationStrategyTest extends PredisTestCase
         );
         );
     }
     }
 
 
+    /**
+     * @group disconnected
+     */
+    public function testGeoradiusCommand()
+    {
+        $profile = Profile\Factory::getDevelopment();
+        $strategy = new ReplicationStrategy();
+
+        $command = $profile->createCommand('GEORADIUS', array('key:geo', 15, 37, 200, 'km'));
+        $this->assertTrue(
+            $strategy->isReadOperation($command),
+            'GEORADIUS is expected to be a read operation.'
+        );
+
+        $command = $profile->createCommand('GEORADIUS', array('key:geo', 15, 37, 200, 'km', 'store', 'key:store'));
+        $this->assertFalse(
+            $strategy->isReadOperation($command),
+            'GEORADIUS with STORE is expected to be a write operation.'
+        );
+
+        $command = $profile->createCommand('GEORADIUS', array('key:geo', 15, 37, 200, 'km', 'storedist', 'key:storedist'));
+        $this->assertFalse(
+            $strategy->isReadOperation($command),
+            'GEORADIUS with STOREDIST is expected to be a write operation.'
+        );
+    }
+
     /**
     /**
      * @group disconnected
      * @group disconnected
      * @expectedException \Predis\NotSupportedException
      * @expectedException \Predis\NotSupportedException
@@ -444,6 +471,7 @@ class ReplicationStrategyTest extends PredisTestCase
             'GEOHASH' => 'read',
             'GEOHASH' => 'read',
             'GEOPOS' => 'read',
             'GEOPOS' => 'read',
             'GEODIST' => 'read',
             'GEODIST' => 'read',
+            'GEORADIUS' => 'variable',
         );
         );
 
 
         if (isset($type)) {
         if (isset($type)) {