From 763acd232da1475cbd4a68219ba7e3084fda8003 Mon Sep 17 00:00:00 2001 From: Daniele Alessandri Date: Mon, 23 May 2016 12:43:00 +0200 Subject: [PATCH 01/10] Add new server profile for Redis 3.2 (new stable). --- src/Profile/Factory.php | 1 + src/Profile/RedisUnstable.php | 7 +- src/Profile/RedisVersion320.php | 272 +++++++++++++++++++ tests/Predis/Profile/RedisVersion320Test.php | 195 +++++++++++++ 4 files changed, 470 insertions(+), 5 deletions(-) create mode 100644 src/Profile/RedisVersion320.php create mode 100644 tests/Predis/Profile/RedisVersion320Test.php diff --git a/src/Profile/Factory.php b/src/Profile/Factory.php index bcee3c2c..260fee42 100644 --- a/src/Profile/Factory.php +++ b/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', ); diff --git a/src/Profile/RedisUnstable.php b/src/Profile/RedisUnstable.php index cf6174fe..573cc9e6 100644 --- a/src/Profile/RedisUnstable.php +++ b/src/Profile/RedisUnstable.php @@ -16,7 +16,7 @@ namespace Predis\Profile; * * @author Daniele Alessandri */ -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 )); } } diff --git a/src/Profile/RedisVersion320.php b/src/Profile/RedisVersion320.php new file mode 100644 index 00000000..4c01fb39 --- /dev/null +++ b/src/Profile/RedisVersion320.php @@ -0,0 +1,272 @@ + + * + * 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 + */ +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', + ); + } +} diff --git a/tests/Predis/Profile/RedisVersion320Test.php b/tests/Predis/Profile/RedisVersion320Test.php new file mode 100644 index 00000000..d5f641f5 --- /dev/null +++ b/tests/Predis/Profile/RedisVersion320Test.php @@ -0,0 +1,195 @@ + + * + * 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', + ); + } +} From da009e59dd5a0096ea0b65dbd5f91ff16fe737c3 Mon Sep 17 00:00:00 2001 From: Daniele Alessandri Date: Mon, 23 May 2016 15:53:30 +0200 Subject: [PATCH 02/10] [tests] Test count argument for SPOP in Redis 3.2. --- tests/Predis/Command/SetPopTest.php | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/Predis/Command/SetPopTest.php b/tests/Predis/Command/SetPopTest.php index bb337a02..87b13c26 100644 --- a/tests/Predis/Command/SetPopTest.php +++ b/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 From 3362e474b66c4de8b5639991ec61bae8984a0566 Mon Sep 17 00:00:00 2001 From: Daniele Alessandri Date: Mon, 23 May 2016 16:16:54 +0200 Subject: [PATCH 03/10] Add new command: BITFIELD (Redis 3.2.0). --- src/ClientContextInterface.php | 1 + src/ClientInterface.php | 1 + src/Cluster/ClusterStrategy.php | 1 + src/Command/Processor/KeyPrefixProcessor.php | 1 + src/Command/StringBitField.php | 28 ++++ src/Profile/RedisVersion320.php | 1 + src/Replication/ReplicationStrategy.php | 26 +++ tests/Predis/Cluster/PredisStrategyTest.php | 1 + tests/Predis/Cluster/RedisStrategyTest.php | 1 + .../Processor/KeyPrefixProcessorTest.php | 5 + tests/Predis/Command/StringBitFieldTest.php | 155 ++++++++++++++++++ tests/Predis/Profile/RedisUnstableTest.php | 1 + tests/Predis/Profile/RedisVersion320Test.php | 1 + .../Replication/ReplicationStrategyTest.php | 46 ++++++ 14 files changed, 269 insertions(+) create mode 100644 src/Command/StringBitField.php create mode 100644 tests/Predis/Command/StringBitFieldTest.php diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index 9c69f5cb..e2625bcc 100644 --- a/src/ClientContextInterface.php +++ b/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) diff --git a/src/ClientInterface.php b/src/ClientInterface.php index 8126ffec..4c65df0e 100644 --- a/src/ClientInterface.php +++ b/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) diff --git a/src/Cluster/ClusterStrategy.php b/src/Cluster/ClusterStrategy.php index 2e845521..b07eb333 100644 --- a/src/Cluster/ClusterStrategy.php +++ b/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, diff --git a/src/Command/Processor/KeyPrefixProcessor.php b/src/Command/Processor/KeyPrefixProcessor.php index f982a8fe..d20078df 100644 --- a/src/Command/Processor/KeyPrefixProcessor.php +++ b/src/Command/Processor/KeyPrefixProcessor.php @@ -159,6 +159,7 @@ class KeyPrefixProcessor implements ProcessorInterface 'BITPOS' => 'static::first', /* ---------------- Redis 3.2 ---------------- */ 'HSTRLEN' => 'static::first', + 'BITFIELD' => 'static::first', ); } diff --git a/src/Command/StringBitField.php b/src/Command/StringBitField.php new file mode 100644 index 00000000..9f4deaa6 --- /dev/null +++ b/src/Command/StringBitField.php @@ -0,0 +1,28 @@ + + * + * 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 + */ +class StringBitField extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BITFIELD'; + } +} diff --git a/src/Profile/RedisVersion320.php b/src/Profile/RedisVersion320.php index 4c01fb39..7c7a4342 100644 --- a/src/Profile/RedisVersion320.php +++ b/src/Profile/RedisVersion320.php @@ -267,6 +267,7 @@ class RedisVersion320 extends RedisProfile /* commands operating on hashes */ 'HSTRLEN' => 'Predis\Command\HashStringLength', + 'BITFIELD' => 'Predis\Command\StringBitField', ); } } diff --git a/src/Replication/ReplicationStrategy.php b/src/Replication/ReplicationStrategy.php index 0b769405..a1c5840f 100644 --- a/src/Replication/ReplicationStrategy.php +++ b/src/Replication/ReplicationStrategy.php @@ -104,6 +104,31 @@ class ReplicationStrategy return ($c = count($arguments)) === 1 ? true : $arguments[$c - 2] !== 'STORE'; } + /** + * 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; + } + /** * Marks a command as a read-only operation. * @@ -232,6 +257,7 @@ class ReplicationStrategy 'TIME' => true, 'PFCOUNT' => true, 'SORT' => array($this, 'isSortReadOnly'), + 'BITFIELD' => array($this, 'isBitfieldReadOnly'), ); } } diff --git a/tests/Predis/Cluster/PredisStrategyTest.php b/tests/Predis/Cluster/PredisStrategyTest.php index 6e944fe4..43d6b155 100644 --- a/tests/Predis/Cluster/PredisStrategyTest.php +++ b/tests/Predis/Cluster/PredisStrategyTest.php @@ -292,6 +292,7 @@ class PredisStrategyTest extends PredisTestCase 'SUBSTR' => 'keys-first', 'BITOP' => 'keys-bitop', 'BITCOUNT' => 'keys-first', + 'BITFIELD' => 'keys-first', /* commands operating on lists */ 'LINSERT' => 'keys-first', diff --git a/tests/Predis/Cluster/RedisStrategyTest.php b/tests/Predis/Cluster/RedisStrategyTest.php index d8b9a544..3a03a4ab 100644 --- a/tests/Predis/Cluster/RedisStrategyTest.php +++ b/tests/Predis/Cluster/RedisStrategyTest.php @@ -302,6 +302,7 @@ class RedisStrategyTest extends PredisTestCase 'SUBSTR' => 'keys-first', 'BITOP' => 'keys-bitop', 'BITCOUNT' => 'keys-first', + 'BITFIELD' => 'keys-first', /* commands operating on lists */ 'LINSERT' => 'keys-first', diff --git a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php index 0f415cd8..8a754659 100644 --- a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php +++ b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php @@ -868,6 +868,11 @@ class KeyPrefixProcessorTest extends PredisTestCase array('key1', 'key2', 'key3'), array('prefix:key1', 'prefix:key2', 'prefix:key3'), ), + /* ---------------- Redis 3.2 ---------------- */ + array('BITFIELD', + array('key', 'GET', 'u8', '0', 'SET', 'u8', '0', '1'), + array('prefix:key', 'GET', 'u8', '0', 'SET', 'u8', '0', '1'), + ), ); } } diff --git a/tests/Predis/Command/StringBitFieldTest.php b/tests/Predis/Command/StringBitFieldTest.php new file mode 100644 index 00000000..244ed0e0 --- /dev/null +++ b/tests/Predis/Command/StringBitFieldTest.php @@ -0,0 +1,155 @@ + + * + * 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'); + } +} diff --git a/tests/Predis/Profile/RedisUnstableTest.php b/tests/Predis/Profile/RedisUnstableTest.php index 77059386..2bf6eadc 100644 --- a/tests/Predis/Profile/RedisUnstableTest.php +++ b/tests/Predis/Profile/RedisUnstableTest.php @@ -190,6 +190,7 @@ class RedisUnstableTest extends PredisProfileTestCase 149 => 'PFMERGE', 150 => 'COMMAND', 151 => 'HSTRLEN', + 152 => 'BITFIELD', ); } } diff --git a/tests/Predis/Profile/RedisVersion320Test.php b/tests/Predis/Profile/RedisVersion320Test.php index d5f641f5..31bee7b8 100644 --- a/tests/Predis/Profile/RedisVersion320Test.php +++ b/tests/Predis/Profile/RedisVersion320Test.php @@ -190,6 +190,7 @@ class RedisVersion320Test extends PredisProfileTestCase 149 => 'PFMERGE', 150 => 'COMMAND', 151 => 'HSTRLEN', + 152 => 'BITFIELD', ); } } diff --git a/tests/Predis/Replication/ReplicationStrategyTest.php b/tests/Predis/Replication/ReplicationStrategyTest.php index 02d00472..d07b3e53 100644 --- a/tests/Predis/Replication/ReplicationStrategyTest.php +++ b/tests/Predis/Replication/ReplicationStrategyTest.php @@ -94,6 +94,51 @@ 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 * @expectedException \Predis\NotSupportedException @@ -316,6 +361,7 @@ class ReplicationStrategyTest extends PredisTestCase 'SETRANGE' => 'write', 'STRLEN' => 'read', 'SUBSTR' => 'read', + 'BITFIELD' => 'variable', /* commands operating on lists */ 'LINSERT' => 'write', From 7ef9619f5cfc49d133f1e9b7ad5baa4ff93a6100 Mon Sep 17 00:00:00 2001 From: Daniele Alessandri Date: Tue, 24 May 2016 12:24:30 +0200 Subject: [PATCH 04/10] Add new command: GEOADD (Redis 3.2.0). --- src/ClientContextInterface.php | 1 + src/ClientInterface.php | 1 + src/Cluster/ClusterStrategy.php | 3 + src/Command/GeospatialGeoAdd.php | 42 +++++++ src/Command/Processor/KeyPrefixProcessor.php | 1 + src/Profile/RedisVersion320.php | 3 + tests/Predis/Cluster/PredisStrategyTest.php | 3 + tests/Predis/Cluster/RedisStrategyTest.php | 3 + tests/Predis/Command/GeospatialGeoAddTest.php | 106 ++++++++++++++++++ .../Processor/KeyPrefixProcessorTest.php | 4 + tests/Predis/Profile/RedisUnstableTest.php | 1 + tests/Predis/Profile/RedisVersion320Test.php | 1 + .../Replication/ReplicationStrategyTest.php | 3 + 13 files changed, 172 insertions(+) create mode 100644 src/Command/GeospatialGeoAdd.php create mode 100644 tests/Predis/Command/GeospatialGeoAddTest.php diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index e2625bcc..0095bcb9 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -156,6 +156,7 @@ use Predis\Command\CommandInterface; * @method $this slowlog($subcommand, $argument = null) * @method $this time() * @method $this command() + * @method $this geoadd($key, $longitude, $latitude, $member) * * @author Daniele Alessandri */ diff --git a/src/ClientInterface.php b/src/ClientInterface.php index 4c65df0e..8d76bc5c 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -164,6 +164,7 @@ use Predis\Profile\ProfileInterface; * @method mixed slowlog($subcommand, $argument = null) * @method array time() * @method array command() + * @method int geoadd($key, $longitude, $latitude, $member) * * @author Daniele Alessandri */ diff --git a/src/Cluster/ClusterStrategy.php b/src/Cluster/ClusterStrategy.php index b07eb333..b89b0a32 100644 --- a/src/Cluster/ClusterStrategy.php +++ b/src/Cluster/ClusterStrategy.php @@ -165,6 +165,9 @@ abstract class ClusterStrategy implements StrategyInterface /* scripting */ 'EVAL' => array($this, 'getKeyFromScriptingCommands'), 'EVALSHA' => array($this, 'getKeyFromScriptingCommands'), + + /* commands performing geospatial operations */ + 'GEOADD' => $getKeyFromFirstArgument, ); } diff --git a/src/Command/GeospatialGeoAdd.php b/src/Command/GeospatialGeoAdd.php new file mode 100644 index 00000000..adca2ca5 --- /dev/null +++ b/src/Command/GeospatialGeoAdd.php @@ -0,0 +1,42 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Command/Processor/KeyPrefixProcessor.php b/src/Command/Processor/KeyPrefixProcessor.php index d20078df..4ac371d8 100644 --- a/src/Command/Processor/KeyPrefixProcessor.php +++ b/src/Command/Processor/KeyPrefixProcessor.php @@ -160,6 +160,7 @@ class KeyPrefixProcessor implements ProcessorInterface /* ---------------- Redis 3.2 ---------------- */ 'HSTRLEN' => 'static::first', 'BITFIELD' => 'static::first', + 'GEOADD' => 'static::first', ); } diff --git a/src/Profile/RedisVersion320.php b/src/Profile/RedisVersion320.php index 7c7a4342..e66ac1da 100644 --- a/src/Profile/RedisVersion320.php +++ b/src/Profile/RedisVersion320.php @@ -268,6 +268,9 @@ class RedisVersion320 extends RedisProfile /* commands operating on hashes */ 'HSTRLEN' => 'Predis\Command\HashStringLength', 'BITFIELD' => 'Predis\Command\StringBitField', + + /* commands performing geospatial operations */ + 'GEOADD' => 'Predis\Command\GeospatialGeoAdd', ); } } diff --git a/tests/Predis/Cluster/PredisStrategyTest.php b/tests/Predis/Cluster/PredisStrategyTest.php index 43d6b155..db8d49ce 100644 --- a/tests/Predis/Cluster/PredisStrategyTest.php +++ b/tests/Predis/Cluster/PredisStrategyTest.php @@ -377,6 +377,9 @@ class PredisStrategyTest extends PredisTestCase /* scripting */ 'EVAL' => 'keys-script', 'EVALSHA' => 'keys-script', + + /* commands performing geospatial operations */ + 'GEOADD' => 'keys-first', ); if (isset($type)) { diff --git a/tests/Predis/Cluster/RedisStrategyTest.php b/tests/Predis/Cluster/RedisStrategyTest.php index 3a03a4ab..649fc9e0 100644 --- a/tests/Predis/Cluster/RedisStrategyTest.php +++ b/tests/Predis/Cluster/RedisStrategyTest.php @@ -387,6 +387,9 @@ class RedisStrategyTest extends PredisTestCase /* scripting */ 'EVAL' => 'keys-script', 'EVALSHA' => 'keys-script', + + /* commands performing geospatial operations */ + 'GEOADD' => 'keys-first', ); if (isset($type)) { diff --git a/tests/Predis/Command/GeospatialGeoAddTest.php b/tests/Predis/Command/GeospatialGeoAddTest.php new file mode 100644 index 00000000..d2be244a --- /dev/null +++ b/tests/Predis/Command/GeospatialGeoAddTest.php @@ -0,0 +1,106 @@ + + * + * 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'); + } +} diff --git a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php index 8a754659..5b883a4a 100644 --- a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php +++ b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php @@ -873,6 +873,10 @@ class KeyPrefixProcessorTest extends PredisTestCase 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'), + ), ); } } diff --git a/tests/Predis/Profile/RedisUnstableTest.php b/tests/Predis/Profile/RedisUnstableTest.php index 2bf6eadc..5966f5ec 100644 --- a/tests/Predis/Profile/RedisUnstableTest.php +++ b/tests/Predis/Profile/RedisUnstableTest.php @@ -191,6 +191,7 @@ class RedisUnstableTest extends PredisProfileTestCase 150 => 'COMMAND', 151 => 'HSTRLEN', 152 => 'BITFIELD', + 153 => 'GEOADD', ); } } diff --git a/tests/Predis/Profile/RedisVersion320Test.php b/tests/Predis/Profile/RedisVersion320Test.php index 31bee7b8..51b48b91 100644 --- a/tests/Predis/Profile/RedisVersion320Test.php +++ b/tests/Predis/Profile/RedisVersion320Test.php @@ -191,6 +191,7 @@ class RedisVersion320Test extends PredisProfileTestCase 150 => 'COMMAND', 151 => 'HSTRLEN', 152 => 'BITFIELD', + 153 => 'GEOADD', ); } } diff --git a/tests/Predis/Replication/ReplicationStrategyTest.php b/tests/Predis/Replication/ReplicationStrategyTest.php index d07b3e53..1ad30755 100644 --- a/tests/Predis/Replication/ReplicationStrategyTest.php +++ b/tests/Predis/Replication/ReplicationStrategyTest.php @@ -438,6 +438,9 @@ class ReplicationStrategyTest extends PredisTestCase /* scripting */ 'EVAL' => 'write', 'EVALSHA' => 'write', + + /* commands performing geospatial operations */ + 'GEOADD' => 'write', ); if (isset($type)) { From 0b6b3b25589a0518998d7034b6cd1815a14f9301 Mon Sep 17 00:00:00 2001 From: Daniele Alessandri Date: Tue, 24 May 2016 15:00:02 +0200 Subject: [PATCH 05/10] Add new command: GEOHASH (Redis 3.2.0). --- src/ClientContextInterface.php | 1 + src/ClientInterface.php | 1 + src/Cluster/ClusterStrategy.php | 1 + src/Command/GeospatialGeoHash.php | 41 +++++++ src/Command/Processor/KeyPrefixProcessor.php | 1 + src/Profile/RedisVersion320.php | 1 + src/Replication/ReplicationStrategy.php | 1 + tests/Predis/Cluster/PredisStrategyTest.php | 1 + tests/Predis/Cluster/RedisStrategyTest.php | 1 + .../Predis/Command/GeospatialGeoHashTest.php | 102 ++++++++++++++++++ .../Processor/KeyPrefixProcessorTest.php | 4 + tests/Predis/Profile/RedisUnstableTest.php | 1 + tests/Predis/Profile/RedisVersion320Test.php | 1 + .../Replication/ReplicationStrategyTest.php | 1 + 14 files changed, 158 insertions(+) create mode 100644 src/Command/GeospatialGeoHash.php create mode 100644 tests/Predis/Command/GeospatialGeoHashTest.php diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index 0095bcb9..bdf1d756 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -157,6 +157,7 @@ use Predis\Command\CommandInterface; * @method $this time() * @method $this command() * @method $this geoadd($key, $longitude, $latitude, $member) + * @method $this geohash($key, array $members) * * @author Daniele Alessandri */ diff --git a/src/ClientInterface.php b/src/ClientInterface.php index 8d76bc5c..6154f2fc 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -165,6 +165,7 @@ use Predis\Profile\ProfileInterface; * @method array time() * @method array command() * @method int geoadd($key, $longitude, $latitude, $member) + * @method array geohash($key, array $members) * * @author Daniele Alessandri */ diff --git a/src/Cluster/ClusterStrategy.php b/src/Cluster/ClusterStrategy.php index b89b0a32..bdc4dd13 100644 --- a/src/Cluster/ClusterStrategy.php +++ b/src/Cluster/ClusterStrategy.php @@ -168,6 +168,7 @@ abstract class ClusterStrategy implements StrategyInterface /* commands performing geospatial operations */ 'GEOADD' => $getKeyFromFirstArgument, + 'GEOHASH' => $getKeyFromFirstArgument, ); } diff --git a/src/Command/GeospatialGeoHash.php b/src/Command/GeospatialGeoHash.php new file mode 100644 index 00000000..2eccaf4f --- /dev/null +++ b/src/Command/GeospatialGeoHash.php @@ -0,0 +1,41 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Command/Processor/KeyPrefixProcessor.php b/src/Command/Processor/KeyPrefixProcessor.php index 4ac371d8..57113eed 100644 --- a/src/Command/Processor/KeyPrefixProcessor.php +++ b/src/Command/Processor/KeyPrefixProcessor.php @@ -161,6 +161,7 @@ class KeyPrefixProcessor implements ProcessorInterface 'HSTRLEN' => 'static::first', 'BITFIELD' => 'static::first', 'GEOADD' => 'static::first', + 'GEOHASH' => 'static::first', ); } diff --git a/src/Profile/RedisVersion320.php b/src/Profile/RedisVersion320.php index e66ac1da..6a575834 100644 --- a/src/Profile/RedisVersion320.php +++ b/src/Profile/RedisVersion320.php @@ -271,6 +271,7 @@ class RedisVersion320 extends RedisProfile /* commands performing geospatial operations */ 'GEOADD' => 'Predis\Command\GeospatialGeoAdd', + 'GEOHASH' => 'Predis\Command\GeospatialGeoHash', ); } } diff --git a/src/Replication/ReplicationStrategy.php b/src/Replication/ReplicationStrategy.php index a1c5840f..7b768a4a 100644 --- a/src/Replication/ReplicationStrategy.php +++ b/src/Replication/ReplicationStrategy.php @@ -258,6 +258,7 @@ class ReplicationStrategy 'PFCOUNT' => true, 'SORT' => array($this, 'isSortReadOnly'), 'BITFIELD' => array($this, 'isBitfieldReadOnly'), + 'GEOHASH' => true, ); } } diff --git a/tests/Predis/Cluster/PredisStrategyTest.php b/tests/Predis/Cluster/PredisStrategyTest.php index db8d49ce..c76d8bd9 100644 --- a/tests/Predis/Cluster/PredisStrategyTest.php +++ b/tests/Predis/Cluster/PredisStrategyTest.php @@ -380,6 +380,7 @@ class PredisStrategyTest extends PredisTestCase /* commands performing geospatial operations */ 'GEOADD' => 'keys-first', + 'GEOHASH' => 'keys-first', ); if (isset($type)) { diff --git a/tests/Predis/Cluster/RedisStrategyTest.php b/tests/Predis/Cluster/RedisStrategyTest.php index 649fc9e0..4a5f6794 100644 --- a/tests/Predis/Cluster/RedisStrategyTest.php +++ b/tests/Predis/Cluster/RedisStrategyTest.php @@ -390,6 +390,7 @@ class RedisStrategyTest extends PredisTestCase /* commands performing geospatial operations */ 'GEOADD' => 'keys-first', + 'GEOHASH' => 'keys-first', ); if (isset($type)) { diff --git a/tests/Predis/Command/GeospatialGeoHashTest.php b/tests/Predis/Command/GeospatialGeoHashTest.php new file mode 100644 index 00000000..bd354146 --- /dev/null +++ b/tests/Predis/Command/GeospatialGeoHashTest.php @@ -0,0 +1,102 @@ + + * + * 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'); + } +} diff --git a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php index 5b883a4a..72ec7928 100644 --- a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php +++ b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php @@ -877,6 +877,10 @@ class KeyPrefixProcessorTest extends PredisTestCase 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'), + ), ); } } diff --git a/tests/Predis/Profile/RedisUnstableTest.php b/tests/Predis/Profile/RedisUnstableTest.php index 5966f5ec..75990e3c 100644 --- a/tests/Predis/Profile/RedisUnstableTest.php +++ b/tests/Predis/Profile/RedisUnstableTest.php @@ -192,6 +192,7 @@ class RedisUnstableTest extends PredisProfileTestCase 151 => 'HSTRLEN', 152 => 'BITFIELD', 153 => 'GEOADD', + 154 => 'GEOHASH', ); } } diff --git a/tests/Predis/Profile/RedisVersion320Test.php b/tests/Predis/Profile/RedisVersion320Test.php index 51b48b91..a9ab0608 100644 --- a/tests/Predis/Profile/RedisVersion320Test.php +++ b/tests/Predis/Profile/RedisVersion320Test.php @@ -192,6 +192,7 @@ class RedisVersion320Test extends PredisProfileTestCase 151 => 'HSTRLEN', 152 => 'BITFIELD', 153 => 'GEOADD', + 154 => 'GEOHASH', ); } } diff --git a/tests/Predis/Replication/ReplicationStrategyTest.php b/tests/Predis/Replication/ReplicationStrategyTest.php index 1ad30755..8ff2bcac 100644 --- a/tests/Predis/Replication/ReplicationStrategyTest.php +++ b/tests/Predis/Replication/ReplicationStrategyTest.php @@ -441,6 +441,7 @@ class ReplicationStrategyTest extends PredisTestCase /* commands performing geospatial operations */ 'GEOADD' => 'write', + 'GEOHASH' => 'read', ); if (isset($type)) { From c4e0044269ac025de8eb6d392f90db00cda16e67 Mon Sep 17 00:00:00 2001 From: Daniele Alessandri Date: Tue, 24 May 2016 16:04:44 +0200 Subject: [PATCH 06/10] Add new command: GEOPOS (Redis 3.2.0). --- src/ClientContextInterface.php | 1 + src/ClientInterface.php | 1 + src/Cluster/ClusterStrategy.php | 1 + src/Command/GeospatialGeoPos.php | 41 +++++++ src/Command/Processor/KeyPrefixProcessor.php | 1 + src/Profile/RedisVersion320.php | 1 + src/Replication/ReplicationStrategy.php | 1 + tests/Predis/Cluster/PredisStrategyTest.php | 1 + tests/Predis/Cluster/RedisStrategyTest.php | 1 + tests/Predis/Command/GeospatialGeoPosTest.php | 112 ++++++++++++++++++ .../Processor/KeyPrefixProcessorTest.php | 4 + tests/Predis/Profile/RedisUnstableTest.php | 1 + tests/Predis/Profile/RedisVersion320Test.php | 1 + .../Replication/ReplicationStrategyTest.php | 1 + 14 files changed, 168 insertions(+) create mode 100644 src/Command/GeospatialGeoPos.php create mode 100644 tests/Predis/Command/GeospatialGeoPosTest.php diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index bdf1d756..73a52848 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -158,6 +158,7 @@ use Predis\Command\CommandInterface; * @method $this command() * @method $this geoadd($key, $longitude, $latitude, $member) * @method $this geohash($key, array $members) + * @method $this geopos($key, array $members) * * @author Daniele Alessandri */ diff --git a/src/ClientInterface.php b/src/ClientInterface.php index 6154f2fc..1d7a5da0 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -166,6 +166,7 @@ use Predis\Profile\ProfileInterface; * @method array command() * @method int geoadd($key, $longitude, $latitude, $member) * @method array geohash($key, array $members) + * @method array geopos($key, array $members) * * @author Daniele Alessandri */ diff --git a/src/Cluster/ClusterStrategy.php b/src/Cluster/ClusterStrategy.php index bdc4dd13..3180cb39 100644 --- a/src/Cluster/ClusterStrategy.php +++ b/src/Cluster/ClusterStrategy.php @@ -169,6 +169,7 @@ abstract class ClusterStrategy implements StrategyInterface /* commands performing geospatial operations */ 'GEOADD' => $getKeyFromFirstArgument, 'GEOHASH' => $getKeyFromFirstArgument, + 'GEOPOS' => $getKeyFromFirstArgument, ); } diff --git a/src/Command/GeospatialGeoPos.php b/src/Command/GeospatialGeoPos.php new file mode 100644 index 00000000..6b7a9a3d --- /dev/null +++ b/src/Command/GeospatialGeoPos.php @@ -0,0 +1,41 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Command/Processor/KeyPrefixProcessor.php b/src/Command/Processor/KeyPrefixProcessor.php index 57113eed..fd217408 100644 --- a/src/Command/Processor/KeyPrefixProcessor.php +++ b/src/Command/Processor/KeyPrefixProcessor.php @@ -162,6 +162,7 @@ class KeyPrefixProcessor implements ProcessorInterface 'BITFIELD' => 'static::first', 'GEOADD' => 'static::first', 'GEOHASH' => 'static::first', + 'GEOPOS' => 'static::first', ); } diff --git a/src/Profile/RedisVersion320.php b/src/Profile/RedisVersion320.php index 6a575834..4e135718 100644 --- a/src/Profile/RedisVersion320.php +++ b/src/Profile/RedisVersion320.php @@ -272,6 +272,7 @@ class RedisVersion320 extends RedisProfile /* commands performing geospatial operations */ 'GEOADD' => 'Predis\Command\GeospatialGeoAdd', 'GEOHASH' => 'Predis\Command\GeospatialGeoHash', + 'GEOPOS' => 'Predis\Command\GeospatialGeoPos', ); } } diff --git a/src/Replication/ReplicationStrategy.php b/src/Replication/ReplicationStrategy.php index 7b768a4a..38245c32 100644 --- a/src/Replication/ReplicationStrategy.php +++ b/src/Replication/ReplicationStrategy.php @@ -259,6 +259,7 @@ class ReplicationStrategy 'SORT' => array($this, 'isSortReadOnly'), 'BITFIELD' => array($this, 'isBitfieldReadOnly'), 'GEOHASH' => true, + 'GEOPOS' => true, ); } } diff --git a/tests/Predis/Cluster/PredisStrategyTest.php b/tests/Predis/Cluster/PredisStrategyTest.php index c76d8bd9..68ad69f3 100644 --- a/tests/Predis/Cluster/PredisStrategyTest.php +++ b/tests/Predis/Cluster/PredisStrategyTest.php @@ -381,6 +381,7 @@ class PredisStrategyTest extends PredisTestCase /* commands performing geospatial operations */ 'GEOADD' => 'keys-first', 'GEOHASH' => 'keys-first', + 'GEOPOS' => 'keys-first', ); if (isset($type)) { diff --git a/tests/Predis/Cluster/RedisStrategyTest.php b/tests/Predis/Cluster/RedisStrategyTest.php index 4a5f6794..25c9d352 100644 --- a/tests/Predis/Cluster/RedisStrategyTest.php +++ b/tests/Predis/Cluster/RedisStrategyTest.php @@ -391,6 +391,7 @@ class RedisStrategyTest extends PredisTestCase /* commands performing geospatial operations */ 'GEOADD' => 'keys-first', 'GEOHASH' => 'keys-first', + 'GEOPOS' => 'keys-first', ); if (isset($type)) { diff --git a/tests/Predis/Command/GeospatialGeoPosTest.php b/tests/Predis/Command/GeospatialGeoPosTest.php new file mode 100644 index 00000000..18c87638 --- /dev/null +++ b/tests/Predis/Command/GeospatialGeoPosTest.php @@ -0,0 +1,112 @@ + + * + * 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'); + } +} diff --git a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php index 72ec7928..d2d2f777 100644 --- a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php +++ b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php @@ -881,6 +881,10 @@ class KeyPrefixProcessorTest extends PredisTestCase 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'), + ), ); } } diff --git a/tests/Predis/Profile/RedisUnstableTest.php b/tests/Predis/Profile/RedisUnstableTest.php index 75990e3c..5e07bccd 100644 --- a/tests/Predis/Profile/RedisUnstableTest.php +++ b/tests/Predis/Profile/RedisUnstableTest.php @@ -193,6 +193,7 @@ class RedisUnstableTest extends PredisProfileTestCase 152 => 'BITFIELD', 153 => 'GEOADD', 154 => 'GEOHASH', + 155 => 'GEOPOS', ); } } diff --git a/tests/Predis/Profile/RedisVersion320Test.php b/tests/Predis/Profile/RedisVersion320Test.php index a9ab0608..d9346ad7 100644 --- a/tests/Predis/Profile/RedisVersion320Test.php +++ b/tests/Predis/Profile/RedisVersion320Test.php @@ -193,6 +193,7 @@ class RedisVersion320Test extends PredisProfileTestCase 152 => 'BITFIELD', 153 => 'GEOADD', 154 => 'GEOHASH', + 155 => 'GEOPOS', ); } } diff --git a/tests/Predis/Replication/ReplicationStrategyTest.php b/tests/Predis/Replication/ReplicationStrategyTest.php index 8ff2bcac..b7cedaf8 100644 --- a/tests/Predis/Replication/ReplicationStrategyTest.php +++ b/tests/Predis/Replication/ReplicationStrategyTest.php @@ -442,6 +442,7 @@ class ReplicationStrategyTest extends PredisTestCase /* commands performing geospatial operations */ 'GEOADD' => 'write', 'GEOHASH' => 'read', + 'GEOPOS' => 'read', ); if (isset($type)) { From b2284d015f6d351bc3d42daa28b94c5dbfc9e7d9 Mon Sep 17 00:00:00 2001 From: Daniele Alessandri Date: Tue, 24 May 2016 16:23:07 +0200 Subject: [PATCH 07/10] Add new command: GEODIST (Redis 3.2.0). --- src/ClientContextInterface.php | 1 + src/ClientInterface.php | 1 + src/Cluster/ClusterStrategy.php | 1 + src/Command/GeospatialGeoDist.php | 28 ++++++ src/Command/Processor/KeyPrefixProcessor.php | 1 + src/Profile/RedisVersion320.php | 1 + src/Replication/ReplicationStrategy.php | 1 + tests/Predis/Cluster/PredisStrategyTest.php | 1 + tests/Predis/Cluster/RedisStrategyTest.php | 1 + .../Predis/Command/GeospatialGeoDistTest.php | 88 +++++++++++++++++++ .../Processor/KeyPrefixProcessorTest.php | 4 + tests/Predis/Profile/RedisUnstableTest.php | 1 + tests/Predis/Profile/RedisVersion320Test.php | 1 + .../Replication/ReplicationStrategyTest.php | 1 + 14 files changed, 131 insertions(+) create mode 100644 src/Command/GeospatialGeoDist.php create mode 100644 tests/Predis/Command/GeospatialGeoDistTest.php diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index 73a52848..06df7920 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -159,6 +159,7 @@ use Predis\Command\CommandInterface; * @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) * * @author Daniele Alessandri */ diff --git a/src/ClientInterface.php b/src/ClientInterface.php index 1d7a5da0..bf136598 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -167,6 +167,7 @@ use Predis\Profile\ProfileInterface; * @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) * * @author Daniele Alessandri */ diff --git a/src/Cluster/ClusterStrategy.php b/src/Cluster/ClusterStrategy.php index 3180cb39..eec40762 100644 --- a/src/Cluster/ClusterStrategy.php +++ b/src/Cluster/ClusterStrategy.php @@ -170,6 +170,7 @@ abstract class ClusterStrategy implements StrategyInterface 'GEOADD' => $getKeyFromFirstArgument, 'GEOHASH' => $getKeyFromFirstArgument, 'GEOPOS' => $getKeyFromFirstArgument, + 'GEODIST' => $getKeyFromFirstArgument, ); } diff --git a/src/Command/GeospatialGeoDist.php b/src/Command/GeospatialGeoDist.php new file mode 100644 index 00000000..17c5f549 --- /dev/null +++ b/src/Command/GeospatialGeoDist.php @@ -0,0 +1,28 @@ + + * + * 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 + */ +class GeospatialGeoDist extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GEODIST'; + } +} diff --git a/src/Command/Processor/KeyPrefixProcessor.php b/src/Command/Processor/KeyPrefixProcessor.php index fd217408..6ce6cf9f 100644 --- a/src/Command/Processor/KeyPrefixProcessor.php +++ b/src/Command/Processor/KeyPrefixProcessor.php @@ -163,6 +163,7 @@ class KeyPrefixProcessor implements ProcessorInterface 'GEOADD' => 'static::first', 'GEOHASH' => 'static::first', 'GEOPOS' => 'static::first', + 'GEODIST' => 'static::first', ); } diff --git a/src/Profile/RedisVersion320.php b/src/Profile/RedisVersion320.php index 4e135718..465d7c97 100644 --- a/src/Profile/RedisVersion320.php +++ b/src/Profile/RedisVersion320.php @@ -273,6 +273,7 @@ class RedisVersion320 extends RedisProfile 'GEOADD' => 'Predis\Command\GeospatialGeoAdd', 'GEOHASH' => 'Predis\Command\GeospatialGeoHash', 'GEOPOS' => 'Predis\Command\GeospatialGeoPos', + 'GEODIST' => 'Predis\Command\GeospatialGeoDist', ); } } diff --git a/src/Replication/ReplicationStrategy.php b/src/Replication/ReplicationStrategy.php index 38245c32..4cde9e72 100644 --- a/src/Replication/ReplicationStrategy.php +++ b/src/Replication/ReplicationStrategy.php @@ -260,6 +260,7 @@ class ReplicationStrategy 'BITFIELD' => array($this, 'isBitfieldReadOnly'), 'GEOHASH' => true, 'GEOPOS' => true, + 'GEODIST' => true, ); } } diff --git a/tests/Predis/Cluster/PredisStrategyTest.php b/tests/Predis/Cluster/PredisStrategyTest.php index 68ad69f3..7d4a020c 100644 --- a/tests/Predis/Cluster/PredisStrategyTest.php +++ b/tests/Predis/Cluster/PredisStrategyTest.php @@ -382,6 +382,7 @@ class PredisStrategyTest extends PredisTestCase 'GEOADD' => 'keys-first', 'GEOHASH' => 'keys-first', 'GEOPOS' => 'keys-first', + 'GEODIST' => 'keys-first', ); if (isset($type)) { diff --git a/tests/Predis/Cluster/RedisStrategyTest.php b/tests/Predis/Cluster/RedisStrategyTest.php index 25c9d352..54cb43cd 100644 --- a/tests/Predis/Cluster/RedisStrategyTest.php +++ b/tests/Predis/Cluster/RedisStrategyTest.php @@ -392,6 +392,7 @@ class RedisStrategyTest extends PredisTestCase 'GEOADD' => 'keys-first', 'GEOHASH' => 'keys-first', 'GEOPOS' => 'keys-first', + 'GEODIST' => 'keys-first', ); if (isset($type)) { diff --git a/tests/Predis/Command/GeospatialGeoDistTest.php b/tests/Predis/Command/GeospatialGeoDistTest.php new file mode 100644 index 00000000..33250a3c --- /dev/null +++ b/tests/Predis/Command/GeospatialGeoDistTest.php @@ -0,0 +1,88 @@ + + * + * 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'); + } +} diff --git a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php index d2d2f777..9357c0f0 100644 --- a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php +++ b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php @@ -885,6 +885,10 @@ class KeyPrefixProcessorTest extends PredisTestCase 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'), + ), ); } } diff --git a/tests/Predis/Profile/RedisUnstableTest.php b/tests/Predis/Profile/RedisUnstableTest.php index 5e07bccd..cf412282 100644 --- a/tests/Predis/Profile/RedisUnstableTest.php +++ b/tests/Predis/Profile/RedisUnstableTest.php @@ -194,6 +194,7 @@ class RedisUnstableTest extends PredisProfileTestCase 153 => 'GEOADD', 154 => 'GEOHASH', 155 => 'GEOPOS', + 156 => 'GEODIST', ); } } diff --git a/tests/Predis/Profile/RedisVersion320Test.php b/tests/Predis/Profile/RedisVersion320Test.php index d9346ad7..99bff7f2 100644 --- a/tests/Predis/Profile/RedisVersion320Test.php +++ b/tests/Predis/Profile/RedisVersion320Test.php @@ -194,6 +194,7 @@ class RedisVersion320Test extends PredisProfileTestCase 153 => 'GEOADD', 154 => 'GEOHASH', 155 => 'GEOPOS', + 156 => 'GEODIST', ); } } diff --git a/tests/Predis/Replication/ReplicationStrategyTest.php b/tests/Predis/Replication/ReplicationStrategyTest.php index b7cedaf8..ce2dcbbe 100644 --- a/tests/Predis/Replication/ReplicationStrategyTest.php +++ b/tests/Predis/Replication/ReplicationStrategyTest.php @@ -443,6 +443,7 @@ class ReplicationStrategyTest extends PredisTestCase 'GEOADD' => 'write', 'GEOHASH' => 'read', 'GEOPOS' => 'read', + 'GEODIST' => 'read', ); if (isset($type)) { From 00000fde4e19690c7035cb8fbb9984f4fca4f287 Mon Sep 17 00:00:00 2001 From: Daniele Alessandri Date: Tue, 24 May 2016 17:09:06 +0200 Subject: [PATCH 08/10] Add new command: GEORADIUS (Redis 3.2.0). --- src/ClientContextInterface.php | 1 + src/ClientInterface.php | 1 + src/Cluster/ClusterStrategy.php | 33 ++++ src/Command/GeospatialGeoRadius.php | 71 ++++++++ src/Command/Processor/KeyPrefixProcessor.php | 28 +++ src/Profile/RedisVersion320.php | 1 + src/Replication/ReplicationStrategy.php | 26 +++ tests/Predis/Cluster/PredisStrategyTest.php | 18 ++ tests/Predis/Cluster/RedisStrategyTest.php | 18 ++ .../Command/GeospatialGeoRadiusTest.php | 170 ++++++++++++++++++ .../Processor/KeyPrefixProcessorTest.php | 8 + tests/Predis/Profile/RedisUnstableTest.php | 1 + tests/Predis/Profile/RedisVersion320Test.php | 1 + .../Replication/ReplicationStrategyTest.php | 28 +++ 14 files changed, 405 insertions(+) create mode 100644 src/Command/GeospatialGeoRadius.php create mode 100644 tests/Predis/Command/GeospatialGeoRadiusTest.php diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index 06df7920..2b5ba40d 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -160,6 +160,7 @@ use Predis\Command\CommandInterface; * @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) * * @author Daniele Alessandri */ diff --git a/src/ClientInterface.php b/src/ClientInterface.php index bf136598..3db635da 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -168,6 +168,7 @@ use Predis\Profile\ProfileInterface; * @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) * * @author Daniele Alessandri */ diff --git a/src/Cluster/ClusterStrategy.php b/src/Cluster/ClusterStrategy.php index eec40762..4f826e52 100644 --- a/src/Cluster/ClusterStrategy.php +++ b/src/Cluster/ClusterStrategy.php @@ -171,6 +171,7 @@ abstract class ClusterStrategy implements StrategyInterface 'GEOHASH' => $getKeyFromFirstArgument, 'GEOPOS' => $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. * diff --git a/src/Command/GeospatialGeoRadius.php b/src/Command/GeospatialGeoRadius.php new file mode 100644 index 00000000..f2052148 --- /dev/null +++ b/src/Command/GeospatialGeoRadius.php @@ -0,0 +1,71 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Command/Processor/KeyPrefixProcessor.php b/src/Command/Processor/KeyPrefixProcessor.php index 6ce6cf9f..53c0a0f0 100644 --- a/src/Command/Processor/KeyPrefixProcessor.php +++ b/src/Command/Processor/KeyPrefixProcessor.php @@ -164,6 +164,7 @@ class KeyPrefixProcessor implements ProcessorInterface 'GEOHASH' => 'static::first', 'GEOPOS' => 'static::first', 'GEODIST' => 'static::first', + 'GEORADIUS' => 'static::georadius', ); } @@ -417,4 +418,31 @@ 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]}"; + + 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); + } + } } diff --git a/src/Profile/RedisVersion320.php b/src/Profile/RedisVersion320.php index 465d7c97..c55d68a5 100644 --- a/src/Profile/RedisVersion320.php +++ b/src/Profile/RedisVersion320.php @@ -274,6 +274,7 @@ class RedisVersion320 extends RedisProfile 'GEOHASH' => 'Predis\Command\GeospatialGeoHash', 'GEOPOS' => 'Predis\Command\GeospatialGeoPos', 'GEODIST' => 'Predis\Command\GeospatialGeoDist', + 'GEORADIUS' => 'Predis\Command\GeospatialGeoRadius', ); } } diff --git a/src/Replication/ReplicationStrategy.php b/src/Replication/ReplicationStrategy.php index 4cde9e72..b4337c37 100644 --- a/src/Replication/ReplicationStrategy.php +++ b/src/Replication/ReplicationStrategy.php @@ -129,6 +129,31 @@ class ReplicationStrategy 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. * @@ -261,6 +286,7 @@ class ReplicationStrategy 'GEOHASH' => true, 'GEOPOS' => true, 'GEODIST' => true, + 'GEORADIUS' => array($this, 'isGeoradiusReadOnly'), ); } } diff --git a/tests/Predis/Cluster/PredisStrategyTest.php b/tests/Predis/Cluster/PredisStrategyTest.php index 7d4a020c..b0108a30 100644 --- a/tests/Predis/Cluster/PredisStrategyTest.php +++ b/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 */ @@ -383,6 +400,7 @@ class PredisStrategyTest extends PredisTestCase 'GEOHASH' => 'keys-first', 'GEOPOS' => 'keys-first', 'GEODIST' => 'keys-first', + 'GEORADIUS' => 'keys-georadius', ); if (isset($type)) { diff --git a/tests/Predis/Cluster/RedisStrategyTest.php b/tests/Predis/Cluster/RedisStrategyTest.php index 54cb43cd..f1b856ba 100644 --- a/tests/Predis/Cluster/RedisStrategyTest.php +++ b/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 */ @@ -393,6 +410,7 @@ class RedisStrategyTest extends PredisTestCase 'GEOHASH' => 'keys-first', 'GEOPOS' => 'keys-first', 'GEODIST' => 'keys-first', + 'GEORADIUS' => 'keys-georadius', ); if (isset($type)) { diff --git a/tests/Predis/Command/GeospatialGeoRadiusTest.php b/tests/Predis/Command/GeospatialGeoRadiusTest.php new file mode 100644 index 00000000..83d74bed --- /dev/null +++ b/tests/Predis/Command/GeospatialGeoRadiusTest.php @@ -0,0 +1,170 @@ + + * + * 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'); + } +} diff --git a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php index 9357c0f0..7de8802b 100644 --- a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php +++ b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php @@ -889,6 +889,14 @@ class KeyPrefixProcessorTest extends PredisTestCase 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'), + ), ); } } diff --git a/tests/Predis/Profile/RedisUnstableTest.php b/tests/Predis/Profile/RedisUnstableTest.php index cf412282..13dd93de 100644 --- a/tests/Predis/Profile/RedisUnstableTest.php +++ b/tests/Predis/Profile/RedisUnstableTest.php @@ -195,6 +195,7 @@ class RedisUnstableTest extends PredisProfileTestCase 154 => 'GEOHASH', 155 => 'GEOPOS', 156 => 'GEODIST', + 157 => 'GEORADIUS', ); } } diff --git a/tests/Predis/Profile/RedisVersion320Test.php b/tests/Predis/Profile/RedisVersion320Test.php index 99bff7f2..28780a1e 100644 --- a/tests/Predis/Profile/RedisVersion320Test.php +++ b/tests/Predis/Profile/RedisVersion320Test.php @@ -195,6 +195,7 @@ class RedisVersion320Test extends PredisProfileTestCase 154 => 'GEOHASH', 155 => 'GEOPOS', 156 => 'GEODIST', + 157 => 'GEORADIUS', ); } } diff --git a/tests/Predis/Replication/ReplicationStrategyTest.php b/tests/Predis/Replication/ReplicationStrategyTest.php index ce2dcbbe..8caae3b6 100644 --- a/tests/Predis/Replication/ReplicationStrategyTest.php +++ b/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 * @expectedException \Predis\NotSupportedException @@ -444,6 +471,7 @@ class ReplicationStrategyTest extends PredisTestCase 'GEOHASH' => 'read', 'GEOPOS' => 'read', 'GEODIST' => 'read', + 'GEORADIUS' => 'variable', ); if (isset($type)) { From 51932d82e898c74f64c14bd4020f53584dec5a4a Mon Sep 17 00:00:00 2001 From: Daniele Alessandri Date: Tue, 24 May 2016 18:58:07 +0200 Subject: [PATCH 09/10] Add new command: GEORADIUSBYMEMBER (Redis 3.2.0). --- src/ClientContextInterface.php | 1 + src/ClientInterface.php | 1 + src/Cluster/ClusterStrategy.php | 8 +- src/Command/GeospatialGeoRadiusByMember.php | 28 +++ src/Command/Processor/KeyPrefixProcessor.php | 6 +- src/Profile/RedisVersion320.php | 1 + src/Replication/ReplicationStrategy.php | 6 +- tests/Predis/Cluster/PredisStrategyTest.php | 18 ++ tests/Predis/Cluster/RedisStrategyTest.php | 18 ++ .../GeospatialGeoRadiusByMemberTest.php | 168 ++++++++++++++++++ .../Processor/KeyPrefixProcessorTest.php | 8 + tests/Predis/Profile/RedisUnstableTest.php | 1 + tests/Predis/Profile/RedisVersion320Test.php | 1 + .../Replication/ReplicationStrategyTest.php | 28 +++ 14 files changed, 286 insertions(+), 7 deletions(-) create mode 100644 src/Command/GeospatialGeoRadiusByMember.php create mode 100644 tests/Predis/Command/GeospatialGeoRadiusByMemberTest.php diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index 2b5ba40d..41e81255 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -161,6 +161,7 @@ use Predis\Command\CommandInterface; * @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 */ diff --git a/src/ClientInterface.php b/src/ClientInterface.php index 3db635da..a7410e99 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -169,6 +169,7 @@ use Predis\Profile\ProfileInterface; * @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 */ diff --git a/src/Cluster/ClusterStrategy.php b/src/Cluster/ClusterStrategy.php index 4f826e52..8d8ae8e8 100644 --- a/src/Cluster/ClusterStrategy.php +++ b/src/Cluster/ClusterStrategy.php @@ -172,6 +172,7 @@ abstract class ClusterStrategy implements StrategyInterface 'GEOPOS' => $getKeyFromFirstArgument, 'GEODIST' => $getKeyFromFirstArgument, 'GEORADIUS' => array($this, 'getKeyFromGeoradiusCommands'), + 'GEORADIUSBYMEMBER' => array($this, 'getKeyFromGeoradiusCommands'), ); } @@ -302,7 +303,7 @@ abstract class ClusterStrategy implements StrategyInterface } /** - * Extracts the key from GEORADIUS command. + * Extracts the key from GEORADIUS and GEORADIUSBYMEMBER commands. * * @param CommandInterface $command Command instance. * @@ -312,11 +313,12 @@ abstract class ClusterStrategy implements StrategyInterface { $arguments = $command->getArguments(); $argc = count($arguments); + $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4; - if ($argc > 5) { + if ($argc > $startIndex) { $keys = array($arguments[0]); - for ($i = 5; $i < $argc; $i++) { + for ($i = $startIndex; $i < $argc; $i++) { $argument = strtoupper($arguments[$i]); if ($argument === 'STORE' || $argument === 'STOREDIST') { $keys[] = $arguments[++$i]; diff --git a/src/Command/GeospatialGeoRadiusByMember.php b/src/Command/GeospatialGeoRadiusByMember.php new file mode 100644 index 00000000..abfff7b4 --- /dev/null +++ b/src/Command/GeospatialGeoRadiusByMember.php @@ -0,0 +1,28 @@ + + * + * 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 + */ +class GeospatialGeoRadiusByMember extends GeospatialGeoRadius +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'GEORADIUSBYMEMBER'; + } +} diff --git a/src/Command/Processor/KeyPrefixProcessor.php b/src/Command/Processor/KeyPrefixProcessor.php index 53c0a0f0..a7f2d446 100644 --- a/src/Command/Processor/KeyPrefixProcessor.php +++ b/src/Command/Processor/KeyPrefixProcessor.php @@ -165,6 +165,7 @@ class KeyPrefixProcessor implements ProcessorInterface 'GEOPOS' => 'static::first', 'GEODIST' => 'static::first', 'GEORADIUS' => 'static::georadius', + 'GEORADIUSBYMEMBER' => 'static::georadius', ); } @@ -429,9 +430,10 @@ class KeyPrefixProcessor implements ProcessorInterface { if ($arguments = $command->getArguments()) { $arguments[0] = "$prefix{$arguments[0]}"; + $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4; - if (($count = count($arguments)) > 5) { - for ($i = 5; $i < $count; ++$i) { + if (($count = count($arguments)) > $startIndex) { + for ($i = $startIndex; $i < $count; ++$i) { switch (strtoupper($arguments[$i])) { case 'STORE': case 'STOREDIST': diff --git a/src/Profile/RedisVersion320.php b/src/Profile/RedisVersion320.php index c55d68a5..7de79573 100644 --- a/src/Profile/RedisVersion320.php +++ b/src/Profile/RedisVersion320.php @@ -275,6 +275,7 @@ class RedisVersion320 extends RedisProfile 'GEOPOS' => 'Predis\Command\GeospatialGeoPos', 'GEODIST' => 'Predis\Command\GeospatialGeoDist', 'GEORADIUS' => 'Predis\Command\GeospatialGeoRadius', + 'GEORADIUSBYMEMBER' => 'Predis\Command\GeospatialGeoRadiusByMember', ); } } diff --git a/src/Replication/ReplicationStrategy.php b/src/Replication/ReplicationStrategy.php index b4337c37..fd6abac8 100644 --- a/src/Replication/ReplicationStrategy.php +++ b/src/Replication/ReplicationStrategy.php @@ -141,9 +141,10 @@ class ReplicationStrategy { $arguments = $command->getArguments(); $argc = count($arguments); + $startIndex = $command->getId() === 'GEORADIUS' ? 5 : 4; - if ($argc > 5) { - for ($i = 5; $i < $argc; $i++) { + if ($argc > $startIndex) { + for ($i = $startIndex; $i < $argc; $i++) { $argument = strtoupper($arguments[$i]); if ($argument === 'STORE' || $argument === 'STOREDIST') { return false; @@ -287,6 +288,7 @@ class ReplicationStrategy 'GEOPOS' => true, 'GEODIST' => true, 'GEORADIUS' => array($this, 'isGeoradiusReadOnly'), + 'GEORADIUSBYMEMBER' => array($this, 'isGeoradiusReadOnly'), ); } } diff --git a/tests/Predis/Cluster/PredisStrategyTest.php b/tests/Predis/Cluster/PredisStrategyTest.php index b0108a30..40e45708 100644 --- a/tests/Predis/Cluster/PredisStrategyTest.php +++ b/tests/Predis/Cluster/PredisStrategyTest.php @@ -169,6 +169,23 @@ class PredisStrategyTest extends PredisTestCase $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 */ @@ -401,6 +418,7 @@ class PredisStrategyTest extends PredisTestCase 'GEOPOS' => 'keys-first', 'GEODIST' => 'keys-first', 'GEORADIUS' => 'keys-georadius', + 'GEORADIUSBYMEMBER' => 'keys-georadius', ); if (isset($type)) { diff --git a/tests/Predis/Cluster/RedisStrategyTest.php b/tests/Predis/Cluster/RedisStrategyTest.php index f1b856ba..216c519e 100644 --- a/tests/Predis/Cluster/RedisStrategyTest.php +++ b/tests/Predis/Cluster/RedisStrategyTest.php @@ -182,6 +182,23 @@ class RedisStrategyTest extends PredisTestCase $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 */ @@ -411,6 +428,7 @@ class RedisStrategyTest extends PredisTestCase 'GEOPOS' => 'keys-first', 'GEODIST' => 'keys-first', 'GEORADIUS' => 'keys-georadius', + 'GEORADIUSBYMEMBER' => 'keys-georadius', ); if (isset($type)) { diff --git a/tests/Predis/Command/GeospatialGeoRadiusByMemberTest.php b/tests/Predis/Command/GeospatialGeoRadiusByMemberTest.php new file mode 100644 index 00000000..b5523495 --- /dev/null +++ b/tests/Predis/Command/GeospatialGeoRadiusByMemberTest.php @@ -0,0 +1,168 @@ + + * + * 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'); + } +} diff --git a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php index 7de8802b..29a5b0e0 100644 --- a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php +++ b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php @@ -897,6 +897,14 @@ class KeyPrefixProcessorTest extends PredisTestCase 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'), + ), ); } } diff --git a/tests/Predis/Profile/RedisUnstableTest.php b/tests/Predis/Profile/RedisUnstableTest.php index 13dd93de..75ffbf58 100644 --- a/tests/Predis/Profile/RedisUnstableTest.php +++ b/tests/Predis/Profile/RedisUnstableTest.php @@ -196,6 +196,7 @@ class RedisUnstableTest extends PredisProfileTestCase 155 => 'GEOPOS', 156 => 'GEODIST', 157 => 'GEORADIUS', + 158 => 'GEORADIUSBYMEMBER', ); } } diff --git a/tests/Predis/Profile/RedisVersion320Test.php b/tests/Predis/Profile/RedisVersion320Test.php index 28780a1e..d68df913 100644 --- a/tests/Predis/Profile/RedisVersion320Test.php +++ b/tests/Predis/Profile/RedisVersion320Test.php @@ -196,6 +196,7 @@ class RedisVersion320Test extends PredisProfileTestCase 155 => 'GEOPOS', 156 => 'GEODIST', 157 => 'GEORADIUS', + 158 => 'GEORADIUSBYMEMBER', ); } } diff --git a/tests/Predis/Replication/ReplicationStrategyTest.php b/tests/Predis/Replication/ReplicationStrategyTest.php index 8caae3b6..b1237c17 100644 --- a/tests/Predis/Replication/ReplicationStrategyTest.php +++ b/tests/Predis/Replication/ReplicationStrategyTest.php @@ -166,6 +166,33 @@ class ReplicationStrategyTest extends PredisTestCase ); } + /** + * @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 @@ -472,6 +499,7 @@ class ReplicationStrategyTest extends PredisTestCase 'GEOPOS' => 'read', 'GEODIST' => 'read', 'GEORADIUS' => 'variable', + 'GEORADIUSBYMEMBER' => 'variable', ); if (isset($type)) { From 02ed5a2f4e3f30ad772845f4c9e8bacb8482f8c7 Mon Sep 17 00:00:00 2001 From: Daniele Alessandri Date: Tue, 24 May 2016 23:14:58 +0200 Subject: [PATCH 10/10] Update SPOP's @method signature in phpdoc. SPOP accepts the optional "count" argument since Redis 3.2. --- src/ClientContextInterface.php | 2 +- src/ClientInterface.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index 41e81255..a01961a4 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -98,7 +98,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) diff --git a/src/ClientInterface.php b/src/ClientInterface.php index a7410e99..893e5365 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -106,7 +106,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)