Преглед на файлове

Merge branch 'v1.1-commands-redis-3.2'

Daniele Alessandri преди 8 години
родител
ревизия
0dff761a61
променени са 29 файла, в които са добавени 2071 реда и са изтрити 9 реда
  1. 8 1
      src/ClientContextInterface.php
  2. 8 1
      src/ClientInterface.php
  3. 42 0
      src/Cluster/ClusterStrategy.php
  4. 42 0
      src/Command/GeospatialGeoAdd.php
  5. 28 0
      src/Command/GeospatialGeoDist.php
  6. 41 0
      src/Command/GeospatialGeoHash.php
  7. 41 0
      src/Command/GeospatialGeoPos.php
  8. 71 0
      src/Command/GeospatialGeoRadius.php
  9. 28 0
      src/Command/GeospatialGeoRadiusByMember.php
  10. 35 0
      src/Command/Processor/KeyPrefixProcessor.php
  11. 28 0
      src/Command/StringBitField.php
  12. 1 0
      src/Profile/Factory.php
  13. 2 5
      src/Profile/RedisUnstable.php
  14. 281 0
      src/Profile/RedisVersion320.php
  15. 57 0
      src/Replication/ReplicationStrategy.php
  16. 43 0
      tests/Predis/Cluster/PredisStrategyTest.php
  17. 43 0
      tests/Predis/Cluster/RedisStrategyTest.php
  18. 106 0
      tests/Predis/Command/GeospatialGeoAddTest.php
  19. 88 0
      tests/Predis/Command/GeospatialGeoDistTest.php
  20. 102 0
      tests/Predis/Command/GeospatialGeoHashTest.php
  21. 112 0
      tests/Predis/Command/GeospatialGeoPosTest.php
  22. 168 0
      tests/Predis/Command/GeospatialGeoRadiusByMemberTest.php
  23. 170 0
      tests/Predis/Command/GeospatialGeoRadiusTest.php
  24. 36 0
      tests/Predis/Command/Processor/KeyPrefixProcessorTest.php
  25. 18 2
      tests/Predis/Command/SetPopTest.php
  26. 155 0
      tests/Predis/Command/StringBitFieldTest.php
  27. 7 0
      tests/Predis/Profile/RedisUnstableTest.php
  28. 202 0
      tests/Predis/Profile/RedisVersion320Test.php
  29. 108 0
      tests/Predis/Replication/ReplicationStrategyTest.php

+ 8 - 1
src/ClientContextInterface.php

@@ -38,6 +38,7 @@ use Predis\Command\CommandInterface;
  * @method $this append($key, $value)
  * @method $this bitcount($key, $start = null, $end = null)
  * @method $this bitop($operation, $destkey, $key)
+ * @method $this bitfield($key, ...)
  * @method $this decr($key)
  * @method $this decrby($key, $decrement)
  * @method $this get($key)
@@ -98,7 +99,7 @@ use Predis\Command\CommandInterface;
  * @method $this sismember($key, $member)
  * @method $this smembers($key)
  * @method $this smove($source, $destination, $member)
- * @method $this spop($key)
+ * @method $this spop($key, $count = null)
  * @method $this srandmember($key, $count = null)
  * @method $this srem($key, $member)
  * @method $this sscan($key, $cursor, array $options = null)
@@ -156,6 +157,12 @@ use Predis\Command\CommandInterface;
  * @method $this slowlog($subcommand, $argument = null)
  * @method $this time()
  * @method $this command()
+ * @method $this geoadd($key, $longitude, $latitude, $member)
+ * @method $this geohash($key, array $members)
+ * @method $this geopos($key, array $members)
+ * @method $this geodist($key, $member1, $member2, $unit = null)
+ * @method $this georadius($key, $longitude, $latitude, $radius, $unit, array $options = null)
+ * @method $this georadiusbymember($key, $member, $radius, $unit, array $options = null)
  *
  * @author Daniele Alessandri <suppakilla@gmail.com>
  */

+ 8 - 1
src/ClientInterface.php

@@ -46,6 +46,7 @@ use Predis\Profile\ProfileInterface;
  * @method int    append($key, $value)
  * @method int    bitcount($key, $start = null, $end = null)
  * @method int    bitop($operation, $destkey, $key)
+ * @method array  bitfield($key, ...)
  * @method int    decr($key)
  * @method int    decrby($key, $decrement)
  * @method string get($key)
@@ -106,7 +107,7 @@ use Predis\Profile\ProfileInterface;
  * @method int    sismember($key, $member)
  * @method array  smembers($key)
  * @method int    smove($source, $destination, $member)
- * @method string spop($key)
+ * @method string spop($key, $count = null)
  * @method string srandmember($key, $count = null)
  * @method int    srem($key, $member)
  * @method array  sscan($key, $cursor, array $options = null)
@@ -164,6 +165,12 @@ use Predis\Profile\ProfileInterface;
  * @method mixed  slowlog($subcommand, $argument = null)
  * @method array  time()
  * @method array  command()
+ * @method int    geoadd($key, $longitude, $latitude, $member)
+ * @method array  geohash($key, array $members)
+ * @method array  geopos($key, array $members)
+ * @method string geodist($key, $member1, $member2, $unit = null)
+ * @method array  georadius($key, $longitude, $latitude, $radius, $unit, array $options = null)
+ * @method array  georadiusbymember($key, $member, $radius, $unit, array $options = null)
  *
  * @author Daniele Alessandri <suppakilla@gmail.com>
  */

+ 42 - 0
src/Cluster/ClusterStrategy.php

@@ -80,6 +80,7 @@ abstract class ClusterStrategy implements StrategyInterface
             'SUBSTR' => $getKeyFromFirstArgument,
             'BITOP' => array($this, 'getKeyFromBitOp'),
             'BITCOUNT' => $getKeyFromFirstArgument,
+            'BITFIELD' => $getKeyFromFirstArgument,
 
             /* commands operating on lists */
             'LINSERT' => $getKeyFromFirstArgument,
@@ -164,6 +165,14 @@ abstract class ClusterStrategy implements StrategyInterface
             /* scripting */
             'EVAL' => array($this, 'getKeyFromScriptingCommands'),
             'EVALSHA' => array($this, 'getKeyFromScriptingCommands'),
+
+            /* commands performing geospatial operations */
+            'GEOADD' => $getKeyFromFirstArgument,
+            'GEOHASH' => $getKeyFromFirstArgument,
+            'GEOPOS' => $getKeyFromFirstArgument,
+            'GEODIST' => $getKeyFromFirstArgument,
+            'GEORADIUS' => array($this, 'getKeyFromGeoradiusCommands'),
+            'GEORADIUSBYMEMBER' => array($this, 'getKeyFromGeoradiusCommands'),
         );
     }
 
@@ -322,6 +331,39 @@ abstract class ClusterStrategy implements StrategyInterface
         }
     }
 
+    /**
+     * Extracts the key from GEORADIUS and GEORADIUSBYMEMBER commands.
+     *
+     * @param CommandInterface $command Command instance.
+     *
+     * @return string|null
+     */
+    protected function getKeyFromGeoradiusCommands(CommandInterface $command)
+    {
+        $arguments = $command->getArguments();
+        $argc = count($arguments);
+        $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4;
+
+        if ($argc > $startIndex) {
+            $keys = array($arguments[0]);
+
+            for ($i = $startIndex; $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.
      *

+ 42 - 0
src/Command/GeospatialGeoAdd.php

@@ -0,0 +1,42 @@
+<?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/geoadd
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ */
+class GeospatialGeoAdd extends Command
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getId()
+    {
+        return 'GEOADD';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function filterArguments(array $arguments)
+    {
+        if (count($arguments) === 2 && is_array($arguments[1])) {
+            foreach (array_pop($arguments) as $item) {
+                $arguments = array_merge($arguments, $item);
+            }
+        }
+
+        return $arguments;
+    }
+}

+ 28 - 0
src/Command/GeospatialGeoDist.php

@@ -0,0 +1,28 @@
+<?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/geodist
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ */
+class GeospatialGeoDist extends Command
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getId()
+    {
+        return 'GEODIST';
+    }
+}

+ 41 - 0
src/Command/GeospatialGeoHash.php

@@ -0,0 +1,41 @@
+<?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/geohash
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ */
+class GeospatialGeoHash extends Command
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getId()
+    {
+        return 'GEOHASH';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function filterArguments(array $arguments)
+    {
+        if (count($arguments) === 2 && is_array($arguments[1])) {
+            $members = array_pop($arguments);
+            $arguments = array_merge($arguments, $members);
+        }
+
+        return $arguments;
+    }
+}

+ 41 - 0
src/Command/GeospatialGeoPos.php

@@ -0,0 +1,41 @@
+<?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/geopos
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ */
+class GeospatialGeoPos extends Command
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getId()
+    {
+        return 'GEOPOS';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function filterArguments(array $arguments)
+    {
+        if (count($arguments) === 2 && is_array($arguments[1])) {
+            $members = array_pop($arguments);
+            $arguments = array_merge($arguments, $members);
+        }
+
+        return $arguments;
+    }
+}

+ 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/GeospatialGeoRadiusByMember.php

@@ -0,0 +1,28 @@
+<?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/georadiusbymember
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ */
+class GeospatialGeoRadiusByMember extends GeospatialGeoRadius
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getId()
+    {
+        return 'GEORADIUSBYMEMBER';
+    }
+}

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

@@ -159,6 +159,13 @@ class KeyPrefixProcessor implements ProcessorInterface
             'BITPOS' => 'static::first',
             /* ---------------- Redis 3.2 ---------------- */
             'HSTRLEN' => 'static::first',
+            'BITFIELD' => 'static::first',
+            'GEOADD' => 'static::first',
+            'GEOHASH' => 'static::first',
+            'GEOPOS' => 'static::first',
+            'GEODIST' => 'static::first',
+            'GEORADIUS' => 'static::georadius',
+            'GEORADIUSBYMEMBER' => 'static::georadius',
         );
     }
 
@@ -412,4 +419,32 @@ class KeyPrefixProcessor implements ProcessorInterface
             $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]}";
+            $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4;
+
+            if (($count = count($arguments)) > $startIndex) {
+                for ($i = $startIndex; $i < $count; ++$i) {
+                    switch (strtoupper($arguments[$i])) {
+                        case 'STORE':
+                        case 'STOREDIST':
+                            $arguments[$i] = "$prefix{$arguments[++$i]}";
+                            break;
+
+                    }
+                }
+            }
+
+            $command->setRawArguments($arguments);
+        }
+    }
 }

+ 28 - 0
src/Command/StringBitField.php

@@ -0,0 +1,28 @@
+<?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/bitfield
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ */
+class StringBitField extends Command
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getId()
+    {
+        return 'BITFIELD';
+    }
+}

+ 1 - 0
src/Profile/Factory.php

@@ -27,6 +27,7 @@ final class Factory
         '2.6' => 'Predis\Profile\RedisVersion260',
         '2.8' => 'Predis\Profile\RedisVersion280',
         '3.0' => 'Predis\Profile\RedisVersion300',
+        '3.2' => 'Predis\Profile\RedisVersion320',
         'dev' => 'Predis\Profile\RedisUnstable',
         'default' => 'Predis\Profile\RedisVersion300',
     );

+ 2 - 5
src/Profile/RedisUnstable.php

@@ -16,7 +16,7 @@ namespace Predis\Profile;
  *
  * @author Daniele Alessandri <suppakilla@gmail.com>
  */
-class RedisUnstable extends RedisVersion300
+class RedisUnstable extends RedisVersion320
 {
     /**
      * {@inheritdoc}
@@ -32,10 +32,7 @@ class RedisUnstable extends RedisVersion300
     public function getSupportedCommands()
     {
         return array_merge(parent::getSupportedCommands(), array(
-            /* ---------------- Redis 3.2 ---------------- */
-
-            /* commands operating on hashes */
-            'HSTRLEN' => 'Predis\Command\HashStringLength',
+            // EMPTY
         ));
     }
 }

+ 281 - 0
src/Profile/RedisVersion320.php

@@ -0,0 +1,281 @@
+<?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\Profile;
+
+/**
+ * Server profile for Redis 3.0.
+ *
+ * @author Daniele Alessandri <suppakilla@gmail.com>
+ */
+class RedisVersion320 extends RedisProfile
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getVersion()
+    {
+        return '3.2';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSupportedCommands()
+    {
+        return array(
+            /* ---------------- Redis 1.2 ---------------- */
+
+            /* commands operating on the key space */
+            'EXISTS' => 'Predis\Command\KeyExists',
+            'DEL' => 'Predis\Command\KeyDelete',
+            'TYPE' => 'Predis\Command\KeyType',
+            'KEYS' => 'Predis\Command\KeyKeys',
+            'RANDOMKEY' => 'Predis\Command\KeyRandom',
+            'RENAME' => 'Predis\Command\KeyRename',
+            'RENAMENX' => 'Predis\Command\KeyRenamePreserve',
+            'EXPIRE' => 'Predis\Command\KeyExpire',
+            'EXPIREAT' => 'Predis\Command\KeyExpireAt',
+            'TTL' => 'Predis\Command\KeyTimeToLive',
+            'MOVE' => 'Predis\Command\KeyMove',
+            'SORT' => 'Predis\Command\KeySort',
+            'DUMP' => 'Predis\Command\KeyDump',
+            'RESTORE' => 'Predis\Command\KeyRestore',
+
+            /* commands operating on string values */
+            'SET' => 'Predis\Command\StringSet',
+            'SETNX' => 'Predis\Command\StringSetPreserve',
+            'MSET' => 'Predis\Command\StringSetMultiple',
+            'MSETNX' => 'Predis\Command\StringSetMultiplePreserve',
+            'GET' => 'Predis\Command\StringGet',
+            'MGET' => 'Predis\Command\StringGetMultiple',
+            'GETSET' => 'Predis\Command\StringGetSet',
+            'INCR' => 'Predis\Command\StringIncrement',
+            'INCRBY' => 'Predis\Command\StringIncrementBy',
+            'DECR' => 'Predis\Command\StringDecrement',
+            'DECRBY' => 'Predis\Command\StringDecrementBy',
+
+            /* commands operating on lists */
+            'RPUSH' => 'Predis\Command\ListPushTail',
+            'LPUSH' => 'Predis\Command\ListPushHead',
+            'LLEN' => 'Predis\Command\ListLength',
+            'LRANGE' => 'Predis\Command\ListRange',
+            'LTRIM' => 'Predis\Command\ListTrim',
+            'LINDEX' => 'Predis\Command\ListIndex',
+            'LSET' => 'Predis\Command\ListSet',
+            'LREM' => 'Predis\Command\ListRemove',
+            'LPOP' => 'Predis\Command\ListPopFirst',
+            'RPOP' => 'Predis\Command\ListPopLast',
+            'RPOPLPUSH' => 'Predis\Command\ListPopLastPushHead',
+
+            /* commands operating on sets */
+            'SADD' => 'Predis\Command\SetAdd',
+            'SREM' => 'Predis\Command\SetRemove',
+            'SPOP' => 'Predis\Command\SetPop',
+            'SMOVE' => 'Predis\Command\SetMove',
+            'SCARD' => 'Predis\Command\SetCardinality',
+            'SISMEMBER' => 'Predis\Command\SetIsMember',
+            'SINTER' => 'Predis\Command\SetIntersection',
+            'SINTERSTORE' => 'Predis\Command\SetIntersectionStore',
+            'SUNION' => 'Predis\Command\SetUnion',
+            'SUNIONSTORE' => 'Predis\Command\SetUnionStore',
+            'SDIFF' => 'Predis\Command\SetDifference',
+            'SDIFFSTORE' => 'Predis\Command\SetDifferenceStore',
+            'SMEMBERS' => 'Predis\Command\SetMembers',
+            'SRANDMEMBER' => 'Predis\Command\SetRandomMember',
+
+            /* commands operating on sorted sets */
+            'ZADD' => 'Predis\Command\ZSetAdd',
+            'ZINCRBY' => 'Predis\Command\ZSetIncrementBy',
+            'ZREM' => 'Predis\Command\ZSetRemove',
+            'ZRANGE' => 'Predis\Command\ZSetRange',
+            'ZREVRANGE' => 'Predis\Command\ZSetReverseRange',
+            'ZRANGEBYSCORE' => 'Predis\Command\ZSetRangeByScore',
+            'ZCARD' => 'Predis\Command\ZSetCardinality',
+            'ZSCORE' => 'Predis\Command\ZSetScore',
+            'ZREMRANGEBYSCORE' => 'Predis\Command\ZSetRemoveRangeByScore',
+
+            /* connection related commands */
+            'PING' => 'Predis\Command\ConnectionPing',
+            'AUTH' => 'Predis\Command\ConnectionAuth',
+            'SELECT' => 'Predis\Command\ConnectionSelect',
+            'ECHO' => 'Predis\Command\ConnectionEcho',
+            'QUIT' => 'Predis\Command\ConnectionQuit',
+
+            /* remote server control commands */
+            'INFO' => 'Predis\Command\ServerInfoV26x',
+            'SLAVEOF' => 'Predis\Command\ServerSlaveOf',
+            'MONITOR' => 'Predis\Command\ServerMonitor',
+            'DBSIZE' => 'Predis\Command\ServerDatabaseSize',
+            'FLUSHDB' => 'Predis\Command\ServerFlushDatabase',
+            'FLUSHALL' => 'Predis\Command\ServerFlushAll',
+            'SAVE' => 'Predis\Command\ServerSave',
+            'BGSAVE' => 'Predis\Command\ServerBackgroundSave',
+            'LASTSAVE' => 'Predis\Command\ServerLastSave',
+            'SHUTDOWN' => 'Predis\Command\ServerShutdown',
+            'BGREWRITEAOF' => 'Predis\Command\ServerBackgroundRewriteAOF',
+
+            /* ---------------- Redis 2.0 ---------------- */
+
+            /* commands operating on string values */
+            'SETEX' => 'Predis\Command\StringSetExpire',
+            'APPEND' => 'Predis\Command\StringAppend',
+            'SUBSTR' => 'Predis\Command\StringSubstr',
+
+            /* commands operating on lists */
+            'BLPOP' => 'Predis\Command\ListPopFirstBlocking',
+            'BRPOP' => 'Predis\Command\ListPopLastBlocking',
+
+            /* commands operating on sorted sets */
+            'ZUNIONSTORE' => 'Predis\Command\ZSetUnionStore',
+            'ZINTERSTORE' => 'Predis\Command\ZSetIntersectionStore',
+            'ZCOUNT' => 'Predis\Command\ZSetCount',
+            'ZRANK' => 'Predis\Command\ZSetRank',
+            'ZREVRANK' => 'Predis\Command\ZSetReverseRank',
+            'ZREMRANGEBYRANK' => 'Predis\Command\ZSetRemoveRangeByRank',
+
+            /* commands operating on hashes */
+            'HSET' => 'Predis\Command\HashSet',
+            'HSETNX' => 'Predis\Command\HashSetPreserve',
+            'HMSET' => 'Predis\Command\HashSetMultiple',
+            'HINCRBY' => 'Predis\Command\HashIncrementBy',
+            'HGET' => 'Predis\Command\HashGet',
+            'HMGET' => 'Predis\Command\HashGetMultiple',
+            'HDEL' => 'Predis\Command\HashDelete',
+            'HEXISTS' => 'Predis\Command\HashExists',
+            'HLEN' => 'Predis\Command\HashLength',
+            'HKEYS' => 'Predis\Command\HashKeys',
+            'HVALS' => 'Predis\Command\HashValues',
+            'HGETALL' => 'Predis\Command\HashGetAll',
+
+            /* transactions */
+            'MULTI' => 'Predis\Command\TransactionMulti',
+            'EXEC' => 'Predis\Command\TransactionExec',
+            'DISCARD' => 'Predis\Command\TransactionDiscard',
+
+            /* publish - subscribe */
+            'SUBSCRIBE' => 'Predis\Command\PubSubSubscribe',
+            'UNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribe',
+            'PSUBSCRIBE' => 'Predis\Command\PubSubSubscribeByPattern',
+            'PUNSUBSCRIBE' => 'Predis\Command\PubSubUnsubscribeByPattern',
+            'PUBLISH' => 'Predis\Command\PubSubPublish',
+
+            /* remote server control commands */
+            'CONFIG' => 'Predis\Command\ServerConfig',
+
+            /* ---------------- Redis 2.2 ---------------- */
+
+            /* commands operating on the key space */
+            'PERSIST' => 'Predis\Command\KeyPersist',
+
+            /* commands operating on string values */
+            'STRLEN' => 'Predis\Command\StringStrlen',
+            'SETRANGE' => 'Predis\Command\StringSetRange',
+            'GETRANGE' => 'Predis\Command\StringGetRange',
+            'SETBIT' => 'Predis\Command\StringSetBit',
+            'GETBIT' => 'Predis\Command\StringGetBit',
+
+            /* commands operating on lists */
+            'RPUSHX' => 'Predis\Command\ListPushTailX',
+            'LPUSHX' => 'Predis\Command\ListPushHeadX',
+            'LINSERT' => 'Predis\Command\ListInsert',
+            'BRPOPLPUSH' => 'Predis\Command\ListPopLastPushHeadBlocking',
+
+            /* commands operating on sorted sets */
+            'ZREVRANGEBYSCORE' => 'Predis\Command\ZSetReverseRangeByScore',
+
+            /* transactions */
+            'WATCH' => 'Predis\Command\TransactionWatch',
+            'UNWATCH' => 'Predis\Command\TransactionUnwatch',
+
+            /* remote server control commands */
+            'OBJECT' => 'Predis\Command\ServerObject',
+            'SLOWLOG' => 'Predis\Command\ServerSlowlog',
+
+            /* ---------------- Redis 2.4 ---------------- */
+
+            /* remote server control commands */
+            'CLIENT' => 'Predis\Command\ServerClient',
+
+            /* ---------------- Redis 2.6 ---------------- */
+
+            /* commands operating on the key space */
+            'PTTL' => 'Predis\Command\KeyPreciseTimeToLive',
+            'PEXPIRE' => 'Predis\Command\KeyPreciseExpire',
+            'PEXPIREAT' => 'Predis\Command\KeyPreciseExpireAt',
+            'MIGRATE' => 'Predis\Command\KeyMigrate',
+
+            /* commands operating on string values */
+            'PSETEX' => 'Predis\Command\StringPreciseSetExpire',
+            'INCRBYFLOAT' => 'Predis\Command\StringIncrementByFloat',
+            'BITOP' => 'Predis\Command\StringBitOp',
+            'BITCOUNT' => 'Predis\Command\StringBitCount',
+
+            /* commands operating on hashes */
+            'HINCRBYFLOAT' => 'Predis\Command\HashIncrementByFloat',
+
+            /* scripting */
+            'EVAL' => 'Predis\Command\ServerEval',
+            'EVALSHA' => 'Predis\Command\ServerEvalSHA',
+            'SCRIPT' => 'Predis\Command\ServerScript',
+
+            /* remote server control commands */
+            'TIME' => 'Predis\Command\ServerTime',
+            'SENTINEL' => 'Predis\Command\ServerSentinel',
+
+            /* ---------------- Redis 2.8 ---------------- */
+
+            /* commands operating on the key space */
+            'SCAN' => 'Predis\Command\KeyScan',
+
+            /* commands operating on string values */
+            'BITPOS' => 'Predis\Command\StringBitPos',
+
+            /* commands operating on sets */
+            'SSCAN' => 'Predis\Command\SetScan',
+
+            /* commands operating on sorted sets */
+            'ZSCAN' => 'Predis\Command\ZSetScan',
+            'ZLEXCOUNT' => 'Predis\Command\ZSetLexCount',
+            'ZRANGEBYLEX' => 'Predis\Command\ZSetRangeByLex',
+            'ZREMRANGEBYLEX' => 'Predis\Command\ZSetRemoveRangeByLex',
+            'ZREVRANGEBYLEX' => 'Predis\Command\ZSetReverseRangeByLex',
+
+            /* commands operating on hashes */
+            'HSCAN' => 'Predis\Command\HashScan',
+
+            /* publish - subscribe */
+            'PUBSUB' => 'Predis\Command\PubSubPubsub',
+
+            /* commands operating on HyperLogLog */
+            'PFADD' => 'Predis\Command\HyperLogLogAdd',
+            'PFCOUNT' => 'Predis\Command\HyperLogLogCount',
+            'PFMERGE' => 'Predis\Command\HyperLogLogMerge',
+
+            /* remote server control commands */
+            'COMMAND' => 'Predis\Command\ServerCommand',
+
+            /* ---------------- Redis 3.2 ---------------- */
+
+            /* commands operating on hashes */
+            'HSTRLEN' => 'Predis\Command\HashStringLength',
+            'BITFIELD' => 'Predis\Command\StringBitField',
+
+            /* commands performing geospatial operations */
+            'GEOADD' => 'Predis\Command\GeospatialGeoAdd',
+            'GEOHASH' => 'Predis\Command\GeospatialGeoHash',
+            'GEOPOS' => 'Predis\Command\GeospatialGeoPos',
+            'GEODIST' => 'Predis\Command\GeospatialGeoDist',
+            'GEORADIUS' => 'Predis\Command\GeospatialGeoRadius',
+            'GEORADIUSBYMEMBER' => 'Predis\Command\GeospatialGeoRadiusByMember',
+        );
+    }
+}

+ 57 - 0
src/Replication/ReplicationStrategy.php

@@ -114,6 +114,57 @@ class ReplicationStrategy
         return true;
     }
 
+    /**
+     * Checks if BITFIELD performs a read-only operation by looking for certain
+     * SET and INCRYBY modifiers in the arguments array of the command.
+     *
+     * @param CommandInterface $command Command instance.
+     *
+     * @return bool
+     */
+    protected function isBitfieldReadOnly(CommandInterface $command)
+    {
+        $arguments = $command->getArguments();
+        $argc = count($arguments);
+
+        if ($argc >= 2) {
+            for ($i = 1; $i < $argc; $i++) {
+                $argument = strtoupper($arguments[$i]);
+                if ($argument === 'SET' || $argument === 'INCRBY') {
+                    return false;
+                }
+            }
+        }
+
+        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);
+        $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4;
+
+        if ($argc > $startIndex) {
+            for ($i = $startIndex; $i < $argc; $i++) {
+                $argument = strtoupper($arguments[$i]);
+                if ($argument === 'STORE' || $argument === 'STOREDIST') {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
     /**
      * Marks a command as a read-only operation.
      *
@@ -242,6 +293,12 @@ class ReplicationStrategy
             'TIME' => true,
             'PFCOUNT' => true,
             'SORT' => array($this, 'isSortReadOnly'),
+            'BITFIELD' => array($this, 'isBitfieldReadOnly'),
+            'GEOHASH' => true,
+            'GEOPOS' => true,
+            'GEODIST' => true,
+            'GEORADIUS' => array($this, 'isGeoradiusReadOnly'),
+            'GEORADIUSBYMEMBER' => array($this, 'isGeoradiusReadOnly'),
         );
     }
 }

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

@@ -170,6 +170,40 @@ 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
+     */
+    public function testKeysForGeoradiusByMemberCommand()
+    {
+        $strategy = $this->getClusterStrategy();
+        $profile = Profile\Factory::getDevelopment();
+
+        $commandID = 'GEORADIUSBYMEMBER';
+
+        $command = $profile->createCommand($commandID, array('{key}:1', 'member', 1, 'km'));
+        $this->assertNotNull($strategy->getSlot($command), $commandID);
+
+        $command = $profile->createCommand($commandID, array('{key}:1', 'member', 1, 'km', 'store', '{key}:2', 'storedist', '{key}:3'));
+        $this->assertNotNull($strategy->getSlot($command), $commandID);
+    }
+
     /**
      * @group disconnected
      */
@@ -310,6 +344,7 @@ class PredisStrategyTest extends PredisTestCase
             'SUBSTR' => 'keys-first',
             'BITOP' => 'keys-bitop',
             'BITCOUNT' => 'keys-first',
+            'BITFIELD' => 'keys-first',
 
             /* commands operating on lists */
             'LINSERT' => 'keys-first',
@@ -394,6 +429,14 @@ class PredisStrategyTest extends PredisTestCase
             /* scripting */
             'EVAL' => 'keys-script',
             'EVALSHA' => 'keys-script',
+
+            /* commands performing geospatial operations */
+            'GEOADD' => 'keys-first',
+            'GEOHASH' => 'keys-first',
+            'GEOPOS' => 'keys-first',
+            'GEODIST' => 'keys-first',
+            'GEORADIUS' => 'keys-georadius',
+            'GEORADIUSBYMEMBER' => 'keys-georadius',
         );
 
         if (isset($type)) {

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

@@ -183,6 +183,40 @@ 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
+     */
+    public function testKeysForGeoradiusByMemberCommand()
+    {
+        $strategy = $this->getClusterStrategy();
+        $profile = Profile\Factory::getDevelopment();
+
+        $commandID = 'GEORADIUSBYMEMBER';
+
+        $command = $profile->createCommand($commandID, array('{key}:1', 'member', 1, 'km'));
+        $this->assertNotNull($strategy->getSlot($command), $commandID);
+
+        $command = $profile->createCommand($commandID, array('{key}:1', 'member', 1, 'km', 'store', '{key}:2', 'storedist', '{key}:3'));
+        $this->assertNotNull($strategy->getSlot($command), $commandID);
+    }
+
     /**
      * @group disconnected
      */
@@ -320,6 +354,7 @@ class RedisStrategyTest extends PredisTestCase
             'SUBSTR' => 'keys-first',
             'BITOP' => 'keys-bitop',
             'BITCOUNT' => 'keys-first',
+            'BITFIELD' => 'keys-first',
 
             /* commands operating on lists */
             'LINSERT' => 'keys-first',
@@ -404,6 +439,14 @@ class RedisStrategyTest extends PredisTestCase
             /* scripting */
             'EVAL' => 'keys-script',
             'EVALSHA' => 'keys-script',
+
+            /* commands performing geospatial operations */
+            'GEOADD' => 'keys-first',
+            'GEOHASH' => 'keys-first',
+            'GEOPOS' => 'keys-first',
+            'GEODIST' => 'keys-first',
+            'GEORADIUS' => 'keys-georadius',
+            'GEORADIUSBYMEMBER' => 'keys-georadius',
         );
 
         if (isset($type)) {

+ 106 - 0
tests/Predis/Command/GeospatialGeoAddTest.php

@@ -0,0 +1,106 @@
+<?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 GeospatialGeoAddTest extends PredisCommandTestCase
+{
+    /**
+     * {@inheritdoc}
+     */
+    protected function getExpectedCommand()
+    {
+        return 'Predis\Command\GeospatialGeoAdd';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function getExpectedId()
+    {
+        return 'GEOADD';
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testFilterArguments()
+    {
+        $arguments = array('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania');
+        $expected = array('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania');
+
+        $command = $this->getCommand();
+        $command->setArguments($arguments);
+
+        $this->assertSame($expected, $command->getArguments());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testFilterArgumentsWithMembersAsSingleArray()
+    {
+        $arguments = array('Sicily', array(
+            array('13.361389', '38.115556', 'Palermo'),
+            array('15.087269', '37.502669', 'Catania'),
+        ));
+
+        $expected = array('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania');
+
+        $command = $this->getCommand();
+        $command->setArguments($arguments);
+
+        $this->assertSame($expected, $command->getArguments());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testParseResponse()
+    {
+        $raw = 1;
+        $expected = 1;
+
+        $command = $this->getCommand();
+
+        $this->assertSame($expected, $command->parseResponse($raw));
+    }
+
+    /**
+     * @group connected
+     * @requiresRedisVersion >= 3.2.0
+     */
+    public function testCommandFillsSortedSet()
+    {
+        $redis = $this->getClient();
+
+        $redis->geoadd('Sicily', '13.361389', '38.115556', 'Palermo');
+        $this->assertSame(array('Palermo' => '3479099956230698'), $redis->zrange('Sicily', 0, -1, 'WITHSCORES'));
+    }
+
+    /**
+     * @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->geoadd('Sicily', '13.361389', '38.115556', 'Palermo');
+    }
+}

+ 88 - 0
tests/Predis/Command/GeospatialGeoDistTest.php

@@ -0,0 +1,88 @@
+<?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 GeospatialGeoDistTest extends PredisCommandTestCase
+{
+    /**
+     * {@inheritdoc}
+     */
+    protected function getExpectedCommand()
+    {
+        return 'Predis\Command\GeospatialGeoDist';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function getExpectedId()
+    {
+        return 'GEODIST';
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testFilterArguments()
+    {
+        $arguments = array('key', 'member:1', 'member:2', 'km');
+        $expected = array('key', 'member:1', 'member:2', 'km');
+
+        $command = $this->getCommand();
+        $command->setArguments($arguments);
+
+        $this->assertSame($expected, $command->getArguments());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testParseResponse()
+    {
+        $raw = array('103.31822459492736');
+        $expected = array('103.31822459492736');
+
+        $command = $this->getCommand();
+
+        $this->assertSame($expected, $command->parseResponse($raw));
+    }
+
+    /**
+     * @group connected
+     * @requiresRedisVersion >= 3.2.0
+     */
+    public function testCommandReturnsGeoDistance()
+    {
+        $redis = $this->getClient();
+
+        $redis->geoadd('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania');
+        $this->assertSame('166.2742', $redis->geodist('Sicily', 'Palermo', 'Catania', 'km'));
+    }
+
+    /**
+     * @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->geodist('Sicily', 'Palermo', 'Catania');
+    }
+}

+ 102 - 0
tests/Predis/Command/GeospatialGeoHashTest.php

@@ -0,0 +1,102 @@
+<?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 GeospatialGeoHashTest extends PredisCommandTestCase
+{
+    /**
+     * {@inheritdoc}
+     */
+    protected function getExpectedCommand()
+    {
+        return 'Predis\Command\GeospatialGeoHash';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function getExpectedId()
+    {
+        return 'GEOHASH';
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testFilterArguments()
+    {
+        $arguments = array('key', 'member:1', 'member:2');
+        $expected = array('key', 'member:1', 'member:2');
+
+        $command = $this->getCommand();
+        $command->setArguments($arguments);
+
+        $this->assertSame($expected, $command->getArguments());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testFilterArgumentsWithMembersAsSingleArray()
+    {
+        $arguments = array('key', array('member:1', 'member:2'));
+        $expected = array('key', 'member:1', 'member:2');
+
+        $command = $this->getCommand();
+        $command->setArguments($arguments);
+
+        $this->assertSame($expected, $command->getArguments());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testParseResponse()
+    {
+        $raw = array('sqc8b49rny0', 'sqdtr74hyu0');
+        $expected = array('sqc8b49rny0', 'sqdtr74hyu0');
+
+        $command = $this->getCommand();
+
+        $this->assertSame($expected, $command->parseResponse($raw));
+    }
+
+    /**
+     * @group connected
+     * @requiresRedisVersion >= 3.2.0
+     */
+    public function testCommandReturnsGeoHashes()
+    {
+        $redis = $this->getClient();
+
+        $redis->geoadd('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania');
+        $this->assertSame(array('sqc8b49rny0', 'sqdtr74hyu0'), $redis->geohash('Sicily', 'Palermo', 'Catania'));
+    }
+
+    /**
+     * @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->geohash('Sicily', 'Palermo');
+    }
+}

+ 112 - 0
tests/Predis/Command/GeospatialGeoPosTest.php

@@ -0,0 +1,112 @@
+<?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 GeospatialGeoPosTest extends PredisCommandTestCase
+{
+    /**
+     * {@inheritdoc}
+     */
+    protected function getExpectedCommand()
+    {
+        return 'Predis\Command\GeospatialGeoPos';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function getExpectedId()
+    {
+        return 'GEOPOS';
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testFilterArguments()
+    {
+        $arguments = array('key', 'member:1', 'member:2');
+        $expected = array('key', 'member:1', 'member:2');
+
+        $command = $this->getCommand();
+        $command->setArguments($arguments);
+
+        $this->assertSame($expected, $command->getArguments());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testFilterArgumentsWithMembersAsSingleArray()
+    {
+        $arguments = array('key', array('member:1', 'member:2'));
+        $expected = array('key', 'member:1', 'member:2');
+
+        $command = $this->getCommand();
+        $command->setArguments($arguments);
+
+        $this->assertSame($expected, $command->getArguments());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testParseResponse()
+    {
+        $raw = array(
+            array("13.361389338970184", "38.115556395496299"),
+            array("15.087267458438873", "37.50266842333162"),
+        );
+
+        $expected = array(
+            array("13.361389338970184", "38.115556395496299"),
+            array("15.087267458438873", "37.50266842333162"),
+        );
+
+        $command = $this->getCommand();
+
+        $this->assertSame($expected, $command->parseResponse($raw));
+    }
+
+    /**
+     * @group connected
+     * @requiresRedisVersion >= 3.2.0
+     */
+    public function testCommandReturnsGeoPositions()
+    {
+        $redis = $this->getClient();
+
+        $redis->geoadd('Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania');
+        $this->assertEquals(array(
+            array("13.361389338970184", "38.115556395496299"),
+            array("15.087267458438873", "37.50266842333162"),
+        ), $redis->geopos('Sicily', 'Palermo', 'Catania'));
+    }
+
+    /**
+     * @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->geopos('Sicily', 'Palermo');
+    }
+}

+ 168 - 0
tests/Predis/Command/GeospatialGeoRadiusByMemberTest.php

@@ -0,0 +1,168 @@
+<?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 GeospatialGeoRadiusByMemberTest extends PredisCommandTestCase
+{
+    /**
+     * {@inheritdoc}
+     */
+    protected function getExpectedCommand()
+    {
+        return 'Predis\Command\GeospatialGeoRadiusByMember';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function getExpectedId()
+    {
+        return 'GEORADIUSBYMEMBER';
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testFilterArguments()
+    {
+        $arguments = array(
+            'Sicily', 'Agrigento', 100, 'km',
+            'WITHCOORD', 'WITHDIST', 'WITHHASH', 'COUNT', 1, 'ASC', 'STORE', 'key:store', 'STOREDIST', 'key:storedist'
+        );
+
+        $expected = array(
+            'Sicily', 'Agrigento', 100, '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', 'Agrigento', 100, 'km', array(
+                'store' => 'key:store',
+                'storedist' => 'key:storedist',
+                'withdist' => true,
+                'withcoord' => true,
+                'withhash' => true,
+                'count' => 1,
+                'sort' => 'asc',
+            ),
+        );
+
+        $expected = array(
+            'Sicily', 'Agrigento', 100, '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', 'Agrigento', 100, 'km', array(
+                'store' => 'key:store',
+                'storedist' => 'key:storedist',
+                'withdist' => false,
+                'withcoord' => false,
+                'withhash' => false,
+                'count' => 1,
+                'sort' => 'asc',
+            ),
+        );
+
+        $expected = array('Sicily', 'Agrigento', 100, '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('Agrigento', 'Palermo'),
+        );
+
+        $expected = array(
+            array('Agrigento', 'Palermo'),
+        );
+
+        $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', '13.583333', '37.316667', 'Agrigento');
+        $this->assertEquals(array('Agrigento', 'Palermo'), $redis->georadiusbymember('Sicily', 'Agrigento', 100, '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', '13.583333', '37.316667', 'Agrigento');
+        $this->assertEquals(array(
+            array('Agrigento', '0.0000', array('13.5833314061164856', '37.31666804993816555')),
+            array('Palermo', '90.9778', array('13.361389338970184', '38.115556395496299')),
+        ), $redis->georadiusbymember('Sicily', 'Agrigento', 100, '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->georadiusbymember('Sicily', 'Agrigento', 200, 'km');
+    }
+}

+ 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');
+    }
+}

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

@@ -873,6 +873,42 @@ class KeyPrefixProcessorTest extends PredisTestCase
                 array('key', 'field'),
                 array('prefix:key', 'field'),
             ),
+            array('BITFIELD',
+                array('key', 'GET', 'u8', '0', 'SET', 'u8', '0', '1'),
+                array('prefix:key', 'GET', 'u8', '0', 'SET', 'u8', '0', '1'),
+            ),
+            array('GEOADD',
+                array('key', '13.361389', '38.115556', 'member:1', '15.087269', '37.502669', 'member:2'),
+                array('prefix:key', '13.361389', '38.115556', 'member:1', '15.087269', '37.502669', 'member:2'),
+            ),
+            array('GEOHASH',
+                array('key', 'member:1', 'member:2'),
+                array('prefix:key', 'member:1', 'member:2'),
+            ),
+            array('GEOPOS',
+                array('key', 'member:1', 'member:2'),
+                array('prefix:key', 'member:1', 'member:2'),
+            ),
+            array('GEODIST',
+                array('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'),
+            ),
+            array('GEORADIUSBYMEMBER',
+                array('key', 'member', '100', 'km'),
+                array('prefix:key', 'member', '100', 'km'),
+            ),
+            array('GEORADIUSBYMEMBER',
+                array('key', 'member', '100', 'km', 'WITHDIST', 'STORE', 'key:store', 'STOREDIST', 'key:storedist'),
+                array('prefix:key', 'member', '100', 'km', 'WITHDIST', 'STORE', 'prefix:key:store', 'STOREDIST', 'prefix:key:storedist'),
+            ),
         );
     }
 }

+ 18 - 2
tests/Predis/Command/SetPopTest.php

@@ -38,8 +38,8 @@ class SetPopTest extends PredisCommandTestCase
      */
     public function testFilterArguments()
     {
-        $arguments = array('key');
-        $expected = array('key');
+        $arguments = array('key', 2);
+        $expected = array('key', 2);
 
         $command = $this->getCommand();
         $command->setArguments($arguments);
@@ -70,6 +70,22 @@ class SetPopTest extends PredisCommandTestCase
         $this->assertNull($redis->spop('letters'));
     }
 
+    /**
+     * @group connected
+     * @requiresRedisVersion >= 3.2.0
+     */
+    public function testPopsMoreRandomMembersFromSet()
+    {
+        $redis = $this->getClient();
+
+        $redis->sadd('letters', 'a', 'b', 'c');
+
+        $this->assertSameValues(array('a', 'b', 'c'), $redis->spop('letters', 3));
+        $this->assertEmpty($redis->spop('letters', 3));
+
+        $this->assertNull($redis->spop('letters'));
+    }
+
     /**
      * @group connected
      * @expectedException \Predis\Response\ServerException

+ 155 - 0
tests/Predis/Command/StringBitFieldTest.php

@@ -0,0 +1,155 @@
+<?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-string
+ */
+class StringBitFieldTest extends PredisCommandTestCase
+{
+    /**
+     * {@inheritdoc}
+     */
+    protected function getExpectedCommand()
+    {
+        return 'Predis\Command\StringBitField';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function getExpectedId()
+    {
+        return 'BITFIELD';
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testFilterArguments()
+    {
+        $arguments = array('key');
+        $expected = array('key');
+
+        $command = $this->getCommand();
+        $command->setArguments($arguments);
+
+        $this->assertSame($expected, $command->getArguments());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testFilterMultipleArguments()
+    {
+        $arguments = array('key', 'incrby', 'u2', '100', '1', 'OVERFLOW', 'SAT', 'incrby', 'u2', '102', '1', 'GET', 'u2', '100');
+        $expected = array('key', 'incrby', 'u2', '100', '1', 'OVERFLOW', 'SAT', 'incrby', 'u2', '102', '1', 'GET', 'u2', '100');
+
+        $command = $this->getCommand();
+        $command->setArguments($arguments);
+
+        $this->assertSame($expected, $command->getArguments());
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testParseResponse()
+    {
+        $raw = array(1);
+        $expected = array(1);
+
+        $command = $this->getCommand();
+
+        $this->assertSame($expected, $command->parseResponse($raw));
+    }
+
+    /**
+     * @group disconnected
+     */
+    public function testParseResponseComplex()
+    {
+        $raw = array(1, 0, 3);
+        $expected = array(1, 0, 3);
+
+        $command = $this->getCommand();
+
+        $this->assertSame($expected, $command->parseResponse($raw));
+    }
+
+    /**
+     * @group connected
+     * @requiresRedisVersion >= 3.2.0
+     */
+    public function testBitfieldWithGetModifier()
+    {
+        $redis = $this->getClient();
+
+        $redis->setbit('string', 0, 1);
+        $redis->setbit('string', 8, 1);
+
+        $this->assertSame(array(128), $redis->bitfield('string', 'GET', 'u8', 0));
+        $this->assertSame(array(128, 1, 128), $redis->bitfield('string', 'GET', 'u8', 0, 'GET', 'u8', 1, 'GET', 'u8', 8));
+    }
+
+    /**
+     * @group connected
+     * @requiresRedisVersion >= 3.2.0
+     */
+    public function testBitfieldWithSetModifier()
+    {
+        $redis = $this->getClient();
+
+        $redis->setbit('string', 0, 1);
+        $redis->setbit('string', 8, 1);
+
+        $this->assertSame(array(128), $redis->bitfield('string', 'SET', 'u8', 0, 1));
+        $this->assertSame(array(1, 128), $redis->bitfield('string', 'SET', 'u8', 0, 128, 'SET', 'u8', 8, 1));
+        $this->assertSame(array(1, 128), $redis->bitfield('string', 'SET', 'u8', 8, 128, 'GET', 'u8', 8));
+
+        $this->assertSame("\x80\x80", $redis->get('string'));
+    }
+
+    /**
+     * @group connected
+     * @requiresRedisVersion >= 3.2.0
+     */
+    public function testBitfieldWithIncrbyModifier()
+    {
+        $redis = $this->getClient();
+
+        $redis->setbit('string', 0, 1);
+        $redis->setbit('string', 8, 1);
+
+        $this->assertSame(array(138), $redis->bitfield('string', 'INCRBY', 'u8', 0, 10));
+        $this->assertSame(array(143, 128), $redis->bitfield('string', 'INCRBY', 'u8', 0, 5, 'INCRBY', 'u8', 0, -15));
+
+        $this->assertSame("\x80\x80", $redis->get('string'));
+    }
+
+    /**
+     * @group connected
+     * @requiresRedisVersion >= 3.2.0
+     * @expectedException \Predis\Response\ServerException
+     * @expectedExceptionMessage Operation against a key holding the wrong kind of value
+     */
+    public function testThrowsExceptionOnWrongType()
+    {
+        $this->markTestSkipped('Currently skipped due issues in Redis (see antirez/redis#3259).');
+
+        $redis = $this->getClient();
+
+        $redis->lpush('metavars', 'foo');
+        $redis->bitfield('metavars', 'SET', 'u4', '0', '1');
+    }
+}

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

@@ -190,6 +190,13 @@ class RedisUnstableTest extends PredisProfileTestCase
             149 => 'PFMERGE',
             150 => 'COMMAND',
             151 => 'HSTRLEN',
+            152 => 'BITFIELD',
+            153 => 'GEOADD',
+            154 => 'GEOHASH',
+            155 => 'GEOPOS',
+            156 => 'GEODIST',
+            157 => 'GEORADIUS',
+            158 => 'GEORADIUSBYMEMBER',
         );
     }
 }

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

@@ -0,0 +1,202 @@
+<?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\Profile;
+
+/**
+ *
+ */
+class RedisVersion320Test extends PredisProfileTestCase
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getProfile($version = null)
+    {
+        return new RedisVersion320();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getExpectedVersion()
+    {
+        return '3.2';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getExpectedCommands()
+    {
+        return array(
+            0 => 'EXISTS',
+            1 => 'DEL',
+            2 => 'TYPE',
+            3 => 'KEYS',
+            4 => 'RANDOMKEY',
+            5 => 'RENAME',
+            6 => 'RENAMENX',
+            7 => 'EXPIRE',
+            8 => 'EXPIREAT',
+            9 => 'TTL',
+            10 => 'MOVE',
+            11 => 'SORT',
+            12 => 'DUMP',
+            13 => 'RESTORE',
+            14 => 'SET',
+            15 => 'SETNX',
+            16 => 'MSET',
+            17 => 'MSETNX',
+            18 => 'GET',
+            19 => 'MGET',
+            20 => 'GETSET',
+            21 => 'INCR',
+            22 => 'INCRBY',
+            23 => 'DECR',
+            24 => 'DECRBY',
+            25 => 'RPUSH',
+            26 => 'LPUSH',
+            27 => 'LLEN',
+            28 => 'LRANGE',
+            29 => 'LTRIM',
+            30 => 'LINDEX',
+            31 => 'LSET',
+            32 => 'LREM',
+            33 => 'LPOP',
+            34 => 'RPOP',
+            35 => 'RPOPLPUSH',
+            36 => 'SADD',
+            37 => 'SREM',
+            38 => 'SPOP',
+            39 => 'SMOVE',
+            40 => 'SCARD',
+            41 => 'SISMEMBER',
+            42 => 'SINTER',
+            43 => 'SINTERSTORE',
+            44 => 'SUNION',
+            45 => 'SUNIONSTORE',
+            46 => 'SDIFF',
+            47 => 'SDIFFSTORE',
+            48 => 'SMEMBERS',
+            49 => 'SRANDMEMBER',
+            50 => 'ZADD',
+            51 => 'ZINCRBY',
+            52 => 'ZREM',
+            53 => 'ZRANGE',
+            54 => 'ZREVRANGE',
+            55 => 'ZRANGEBYSCORE',
+            56 => 'ZCARD',
+            57 => 'ZSCORE',
+            58 => 'ZREMRANGEBYSCORE',
+            59 => 'PING',
+            60 => 'AUTH',
+            61 => 'SELECT',
+            62 => 'ECHO',
+            63 => 'QUIT',
+            64 => 'INFO',
+            65 => 'SLAVEOF',
+            66 => 'MONITOR',
+            67 => 'DBSIZE',
+            68 => 'FLUSHDB',
+            69 => 'FLUSHALL',
+            70 => 'SAVE',
+            71 => 'BGSAVE',
+            72 => 'LASTSAVE',
+            73 => 'SHUTDOWN',
+            74 => 'BGREWRITEAOF',
+            75 => 'SETEX',
+            76 => 'APPEND',
+            77 => 'SUBSTR',
+            78 => 'BLPOP',
+            79 => 'BRPOP',
+            80 => 'ZUNIONSTORE',
+            81 => 'ZINTERSTORE',
+            82 => 'ZCOUNT',
+            83 => 'ZRANK',
+            84 => 'ZREVRANK',
+            85 => 'ZREMRANGEBYRANK',
+            86 => 'HSET',
+            87 => 'HSETNX',
+            88 => 'HMSET',
+            89 => 'HINCRBY',
+            90 => 'HGET',
+            91 => 'HMGET',
+            92 => 'HDEL',
+            93 => 'HEXISTS',
+            94 => 'HLEN',
+            95 => 'HKEYS',
+            96 => 'HVALS',
+            97 => 'HGETALL',
+            98 => 'MULTI',
+            99 => 'EXEC',
+            100 => 'DISCARD',
+            101 => 'SUBSCRIBE',
+            102 => 'UNSUBSCRIBE',
+            103 => 'PSUBSCRIBE',
+            104 => 'PUNSUBSCRIBE',
+            105 => 'PUBLISH',
+            106 => 'CONFIG',
+            107 => 'PERSIST',
+            108 => 'STRLEN',
+            109 => 'SETRANGE',
+            110 => 'GETRANGE',
+            111 => 'SETBIT',
+            112 => 'GETBIT',
+            113 => 'RPUSHX',
+            114 => 'LPUSHX',
+            115 => 'LINSERT',
+            116 => 'BRPOPLPUSH',
+            117 => 'ZREVRANGEBYSCORE',
+            118 => 'WATCH',
+            119 => 'UNWATCH',
+            120 => 'OBJECT',
+            121 => 'SLOWLOG',
+            122 => 'CLIENT',
+            123 => 'PTTL',
+            124 => 'PEXPIRE',
+            125 => 'PEXPIREAT',
+            126 => 'MIGRATE',
+            127 => 'PSETEX',
+            128 => 'INCRBYFLOAT',
+            129 => 'BITOP',
+            130 => 'BITCOUNT',
+            131 => 'HINCRBYFLOAT',
+            132 => 'EVAL',
+            133 => 'EVALSHA',
+            134 => 'SCRIPT',
+            135 => 'TIME',
+            136 => 'SENTINEL',
+            137 => 'SCAN',
+            138 => 'BITPOS',
+            139 => 'SSCAN',
+            140 => 'ZSCAN',
+            141 => 'ZLEXCOUNT',
+            142 => 'ZRANGEBYLEX',
+            143 => 'ZREMRANGEBYLEX',
+            144 => 'ZREVRANGEBYLEX',
+            145 => 'HSCAN',
+            146 => 'PUBSUB',
+            147 => 'PFADD',
+            148 => 'PFCOUNT',
+            149 => 'PFMERGE',
+            150 => 'COMMAND',
+            151 => 'HSTRLEN',
+            152 => 'BITFIELD',
+            153 => 'GEOADD',
+            154 => 'GEOHASH',
+            155 => 'GEOPOS',
+            156 => 'GEODIST',
+            157 => 'GEORADIUS',
+            158 => 'GEORADIUSBYMEMBER',
+        );
+    }
+}

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

@@ -94,6 +94,105 @@ class ReplicationStrategyTest extends PredisTestCase
         );
     }
 
+    /**
+     * @group disconnected
+     */
+    public function testBitFieldCommand()
+    {
+        $profile = Profile\Factory::getDevelopment();
+        $strategy = new ReplicationStrategy();
+
+        $command = $profile->createCommand('BITFIELD', array('key'));
+        $this->assertTrue(
+            $strategy->isReadOperation($command),
+            'BITFIELD with no modifiers is expected to be a read operation.'
+        );
+
+        $command = $profile->createCommand('BITFIELD', array('key', 'GET', 'u4', '0'));
+        $this->assertTrue(
+            $strategy->isReadOperation($command),
+            'BITFIELD with GET only is expected to be a read operation.'
+        );
+
+        $command = $profile->createCommand('BITFIELD', array('key', 'SET', 'u4', '0', 1));
+        $this->assertFalse(
+            $strategy->isReadOperation($command),
+            'BITFIELD with SET is expected to be a write operation.'
+        );
+
+        $command = $profile->createCommand('BITFIELD', array('key', 'INCRBY', 'u4', '0', 1));
+        $this->assertFalse(
+            $strategy->isReadOperation($command),
+            'BITFIELD with INCRBY is expected to be a write operation.'
+        );
+
+        $command = $profile->createCommand('BITFIELD', array('key', 'GET', 'u4', '0', 'INCRBY', 'u4', '0', 1));
+        $this->assertFalse(
+            $strategy->isReadOperation($command),
+            'BITFIELD with GET and INCRBY is expected to be a write operation.'
+        );
+
+        $command = $profile->createCommand('BITFIELD', array('key', 'GET', 'u4', '0', 'SET', 'u4', '0', 1));
+        $this->assertFalse(
+            $strategy->isReadOperation($command),
+            'BITFIELD with GET and SET is expected to be a write operation.'
+        );
+    }
+
+    /**
+     * @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
+     */
+    public function testGeoradiusByMemberCommand()
+    {
+        $profile = Profile\Factory::getDevelopment();
+        $strategy = new ReplicationStrategy();
+
+        $command = $profile->createCommand('GEORADIUSBYMEMBER', array('key:geo', 15, 37, 200, 'km'));
+        $this->assertTrue(
+            $strategy->isReadOperation($command),
+            'GEORADIUSBYMEMBER is expected to be a read operation.'
+        );
+
+        $command = $profile->createCommand('GEORADIUSBYMEMBER', array('key:geo', 15, 37, 200, 'km', 'store', 'key:store'));
+        $this->assertFalse(
+            $strategy->isReadOperation($command),
+            'GEORADIUSBYMEMBER with STORE is expected to be a write operation.'
+        );
+
+        $command = $profile->createCommand('GEORADIUSBYMEMBER', array('key:geo', 15, 37, 200, 'km', 'storedist', 'key:storedist'));
+        $this->assertFalse(
+            $strategy->isReadOperation($command),
+            'GEORADIUSBYMEMBER with STOREDIST is expected to be a write operation.'
+        );
+    }
+
     /**
      * @group disconnected
      * @expectedException \Predis\NotSupportedException
@@ -316,6 +415,7 @@ class ReplicationStrategyTest extends PredisTestCase
             'SETRANGE' => 'write',
             'STRLEN' => 'read',
             'SUBSTR' => 'read',
+            'BITFIELD' => 'variable',
 
             /* commands operating on lists */
             'LINSERT' => 'write',
@@ -392,6 +492,14 @@ class ReplicationStrategyTest extends PredisTestCase
             /* scripting */
             'EVAL' => 'write',
             'EVALSHA' => 'write',
+
+            /* commands performing geospatial operations */
+            'GEOADD' => 'write',
+            'GEOHASH' => 'read',
+            'GEOPOS' => 'read',
+            'GEODIST' => 'read',
+            'GEORADIUS' => 'variable',
+            'GEORADIUSBYMEMBER' => 'variable',
         );
 
         if (isset($type)) {