├── .gitignore ├── lib ├── redis-namespace.rb └── redis │ ├── namespace │ └── version.rb │ └── namespace.rb ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── codeql.yml ├── Rakefile ├── Gemfile ├── LICENSE ├── redis-namespace.gemspec ├── spec ├── spec_helper.rb ├── deprecation_spec.rb └── redis_spec.rb ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | pkg 3 | -------------------------------------------------------------------------------- /lib/redis-namespace.rb: -------------------------------------------------------------------------------- 1 | require 'redis/namespace' 2 | -------------------------------------------------------------------------------- /lib/redis/namespace/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | class Redis 4 | class Namespace 5 | VERSION = '1.11.0' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | 9 | - package-ecosystem: "bundler" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require "rspec/core/rake_task" 3 | 4 | require "bundler/gem_tasks" 5 | 6 | RSpec::Core::RakeTask.new(:spec) do |spec| 7 | spec.pattern = 'spec/*_spec.rb' 8 | spec.rspec_opts = ['--backtrace'] 9 | spec.ruby_opts = ['-w'] 10 | end 11 | 12 | task :default => :spec 13 | task :test => :spec 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | case redis_version = ENV.fetch('REDIS_VERSION', 'latest') 4 | when 'latest' 5 | gem 'redis', '~> 4' 6 | else 7 | gem 'redis', "~> #{redis_version}" 8 | end 9 | 10 | platforms :rbx do 11 | # These are the ruby standard library 12 | # dependencies of redis-rb, rake, and rspec. 13 | gem 'rubysl-net-http' 14 | gem 'rubysl-socket' 15 | gem 'rubysl-logger' 16 | gem 'rubysl-cgi' 17 | gem 'rubysl-uri' 18 | gem 'rubysl-timeout' 19 | gem 'rubysl-zlib' 20 | gem 'rubysl-stringio' 21 | end 22 | 23 | gemspec 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2009 Chris Wanstrath 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: "Test on Redis ${{ matrix.redis-version }}, Ruby ${{ matrix.ruby-version }}" 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | redis-version: 13 | - '4.0' 14 | - 4.1 15 | - 4.2 16 | - 4.3 17 | - 4.4 18 | - 4.5 19 | - 4.6 20 | - latest 21 | ruby-version: 22 | - head 23 | - '3.2' 24 | - '3.1' 25 | - '3.0' 26 | - '2.7' 27 | - '2.6' 28 | - '2.5' 29 | - '2.4' 30 | - jruby-9.3 31 | - jruby-9.2 32 | 33 | services: 34 | redis: 35 | image: redis 36 | options: >- 37 | --health-cmd "redis-cli ping" 38 | --health-interval 10s 39 | --health-timeout 5s 40 | --health-retries 5 41 | ports: 42 | - 6379:6379 43 | env: 44 | REDIS_VERSION: "${{ matrix.redis-version }}" 45 | 46 | steps: 47 | - uses: actions/checkout@v3 48 | - name: Set up Ruby ${{ matrix.ruby-version }} 49 | uses: ruby/setup-ruby@v1 50 | with: 51 | ruby-version: ${{ matrix.ruby-version }} 52 | bundler-cache: true # 'bundle install' and cache 53 | 54 | - name: Run tests 55 | run: bundle exec rake 56 | -------------------------------------------------------------------------------- /redis-namespace.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'redis/namespace/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "redis-namespace" 7 | s.version = Redis::Namespace::VERSION 8 | s.date = Time.now.strftime('%Y-%m-%d') 9 | s.summary = "Namespaces Redis commands." 10 | s.homepage = "https://github.com/resque/redis-namespace" 11 | s.email = ["chris@ozmm.org", "hone02@gmail.com", "steve@steveklabnik.com", "me@yaauie.com", "mike@mikebian.co"] 12 | s.authors = ["Chris Wanstrath", "Terence Lee", "Steve Klabnik", "Ryan Biesemeyer", "Mike Bianco"] 13 | s.license = 'MIT' 14 | 15 | s.metadata = { 16 | "bug_tracker_uri" => "https://github.com/resque/redis-namespace/issues", 17 | "changelog_uri" => "https://github.com/resque/redis-namespace/blob/master/CHANGELOG.md", 18 | "documentation_uri" => "https://www.rubydoc.info/gems/redis-namespace/#{s.version}", 19 | "rubygems_mfa_required" => "true" 20 | } 21 | 22 | s.files = %w( README.md Rakefile LICENSE ) 23 | s.files += Dir.glob("lib/**/*") 24 | s.files += Dir.glob("test/**/*") 25 | s.files += Dir.glob("spec/**/*") 26 | 27 | s.required_ruby_version = '>= 2.4' 28 | 29 | s.add_dependency "redis", ">= 4" 30 | 31 | s.add_development_dependency "rake" 32 | s.add_development_dependency "rspec", "~> 3.7" 33 | s.add_development_dependency "rspec-its" 34 | s.add_development_dependency "connection_pool" 35 | 36 | s.description = <= 4 25 | - Added matrix tests on CI for supported redis versions 26 | 27 | ## 1.8.2 28 | 29 | - Fix compatibility with redis-rb 4.6.0. `Redis::Namespace#multi` and `Redis::Namespace#pipelined` were no longer 30 | thread-safe. Calling these methods concurrently on the same instance could cause pipelines or transaction to be 31 | intertwined. See https://github.com/resque/redis-namespace/issues/191 and https://github.com/redis/redis-rb/issues/1088 32 | 33 | ## 1.8.1 34 | 35 | - Allow Ruby 3.0 version in gemspec 36 | 37 | ## 1.8.0 38 | 39 | - Fix `Redis::Namespace#inspect` to include the entire namespaced prefix. 40 | - Support variadic `exists` and `exists?`. 41 | 42 | ## 1.7.0 43 | 44 | - Add `Redis::Namespace.full_namespace` to return the full namespace in case of nested clients. 45 | - Add support for `ZRANGEBYLEX`, `ZREMRANGEBYLEX` and `ZREVRANGEBYLEX`. 46 | - Add support for `BITPOS` command 47 | - Remove deprecated has_rdoc config from gemspec 48 | - Remove EOL rubies from travis.yml 49 | - Add Ruby 2.4 minimum version to gemspec 50 | 51 | ## 1.6.0 52 | 53 | - Support redis-rb 4.0.0 54 | 55 | ## 1.5.1 56 | 57 | - Add support for `UNWATCH` command 58 | - Add support for `REDIS_NAMESPACE_QUIET` environment variable 59 | 60 | ## 1.5.0 61 | 62 | - Fix `brpop` 63 | - Relax dependency of redis-rb to enable users to use redis-rb 3.1 64 | - Add support for HyperLogLog family of commands (`PFADD`, `PFCOUNT`, `PFMERGE`) 65 | - Add (1.x -> 2.x) deprecations and ability to enforce them before upgrading. 66 | 67 | ## 1.4.1 68 | 69 | - Fixed the build for 1.8.7 70 | 71 | ## 1.4.0 72 | 73 | - Add support for `SCAN` family of commands (`HSCAN`, `SSCAN`, `ZSCAN`) 74 | - Add support for redis-rb's `scan_each` method and friends 75 | 76 | ## 1.3.2 77 | 78 | - Fix #68: Capital commands (e.g. redis.send('GET', 'foo')) 79 | - Fix #67: Nested namespace vs. `eval` command 80 | - Fix #65: Require redis ~> 3.0.4 for upstream bugfix 81 | - Feature: Resque::Namespace::VERSION constant 82 | 83 | ## 1.3.1 84 | 85 | - Fix: (Security) don't proxy `exec` through `#method_missing` 86 | - Fix #62: Don't try to remove namespace from `Redis::Future` 87 | - Fix #61: Support `multi` with no block 88 | - Feature #58: Support `echo`, `exec`, `strlen` commands 89 | 90 | ## 1.3.0 91 | 92 | Features: 93 | - Added commands: `multi`, `pipelined`, `mapped_mset`, and `mapped_msetnx` 94 | - Added temporary namespaces that last for the duration of a block 95 | - Unknown commands now warn 96 | - Added `mapped_mset` command 97 | 98 | Also lots of bug fixes. 99 | 100 | ## 1.2.1 101 | 102 | Features: 103 | - make redis connection accessible as a reader 104 | 105 | ## 1.2.0 106 | 107 | Features: 108 | - added mapped_hmset (@hectcastro, #32) 109 | - added append,brpoplpush,getbit,getrange,linsert,lpushx,rpushx,setbit,setrange (@yaauie, #33) 110 | - use Redis.current as default connection (@cldwalker, #29) 111 | - support for redis 3.0.0 (@czarneckid, #39) 112 | -------------------------------------------------------------------------------- /spec/deprecation_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/spec_helper' 2 | 3 | require 'stringio' 4 | 5 | describe Redis::Namespace do 6 | # Blind passthrough of unhandled commands will be removed 7 | # in 2.0; the following tests ensure that we support them 8 | # until that point, & that we can programatically disable 9 | # them in the meantime. 10 | context 'deprecated 1.x behaviour' do 11 | let(:redis) { double(Redis) } 12 | let(:namespaced) do 13 | Redis::Namespace.new(:ns, options.merge(:redis => redis)) 14 | end 15 | 16 | let(:options) { Hash.new } 17 | 18 | subject { namespaced } 19 | 20 | its(:deprecations?) { should be false } 21 | its(:warning?) { should be true } 22 | 23 | context('with REDIS_NAMESPACE_DEPRECATIONS') do 24 | around(:each) {|e| with_env('REDIS_NAMESPACE_DEPRECATIONS'=>'1', &e) } 25 | its(:deprecations?) { should be true } 26 | end 27 | 28 | context('with REDIS_NAMESPACE_QUIET') do 29 | around(:each) {|e| with_env('REDIS_NAMESPACE_QUIET'=>'1', &e) } 30 | its(:warning?) { should be false } 31 | end 32 | 33 | before(:each) do 34 | allow(redis).to receive(:unhandled) do |*args| 35 | "unhandled(#{args.inspect})" 36 | end 37 | allow(redis).to receive(:flushdb).and_return("OK") 38 | end 39 | 40 | # This behaviour will hold true after the 2.x migration 41 | context('with deprecations enabled') do 42 | let(:options) { {:deprecations => true} } 43 | its(:deprecations?) { should be true } 44 | 45 | context('with an unhandled command') do 46 | it { is_expected.not_to respond_to :unhandled } 47 | 48 | it('raises a NoMethodError') do 49 | expect do 50 | namespaced.unhandled('foo') 51 | end.to raise_exception NoMethodError 52 | end 53 | end 54 | 55 | context('with an administrative command') do 56 | it { is_expected.not_to respond_to :flushdb } 57 | 58 | it('raises a NoMethodError') do 59 | expect do 60 | namespaced.flushdb 61 | end.to raise_exception NoMethodError 62 | end 63 | end 64 | end 65 | 66 | # This behaviour will no longer be available after the 2.x migration 67 | context('with deprecations disabled') do 68 | let(:options) { {:deprecations => false} } 69 | its(:deprecations?) { should be false } 70 | 71 | context('with an an unhandled command') do 72 | it { is_expected.to respond_to :unhandled } 73 | 74 | it 'blindly passes through' do 75 | expect(redis).to receive(:unhandled) 76 | 77 | capture_stderr do 78 | response = namespaced.unhandled('foo') 79 | expect(response).to eq 'unhandled(["foo"])' 80 | end 81 | end 82 | 83 | it 'warns with helpful output' do 84 | capture_stderr(stderr = StringIO.new) do 85 | namespaced.unhandled('bar') 86 | end 87 | warning = stderr.tap(&:rewind).read 88 | 89 | expect(warning).to_not be_empty 90 | expect(warning).to include %q(Passing 'unhandled' command to redis as is) 91 | expect(warning).to include %q(blind passthrough) 92 | expect(warning).to include __FILE__ 93 | end 94 | 95 | context('and warnings disabled') do 96 | let(:options) { super().merge(:warning => false)} 97 | it 'does not warn' do 98 | capture_stderr(stderr = StringIO.new) do 99 | namespaced.unhandled('bar') 100 | end 101 | warning = stderr.tap(&:rewind).read 102 | 103 | expect(warning).to be_empty 104 | end 105 | end 106 | end 107 | 108 | context('with an administrative command') do 109 | it { is_expected.to respond_to :flushdb } 110 | it 'processes the command' do 111 | expect(redis).to receive(:flushdb) 112 | capture_stderr { namespaced.flushdb } 113 | end 114 | it 'warns with helpful output' do 115 | capture_stderr(stderr = StringIO.new) do 116 | namespaced.flushdb 117 | end 118 | warning = stderr.tap(&:rewind).read 119 | 120 | expect(warning).to_not be_empty 121 | expect(warning).to include %q(Passing 'flushdb' command to redis as is) 122 | expect(warning).to include %q(administrative) 123 | expect(warning).to include __FILE__ 124 | end 125 | end 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | redis-namespace 2 | =============== 3 | 4 | Redis::Namespace provides an interface to a namespaced subset of your [redis][] keyspace (e.g., keys with a common beginning), and requires the [redis-rb][] gem. 5 | 6 | ```ruby 7 | require 'redis-namespace' 8 | # => true 9 | 10 | redis_connection = Redis.new 11 | # => # 12 | namespaced_redis = Redis::Namespace.new(:ns, redis: redis_connection) 13 | # => # 14 | 15 | namespaced_redis.set('foo', 'bar') # redis_connection.set('ns:foo', 'bar') 16 | # => "OK" 17 | 18 | # Redis::Namespace automatically prepended our namespace to the key 19 | # before sending it to our redis client. 20 | 21 | namespaced_redis.get('foo') 22 | # => "bar" 23 | redis_connection.get('foo') 24 | # => nil 25 | redis_connection.get('ns:foo') 26 | # => "bar" 27 | 28 | namespaced_redis.del('foo') 29 | # => 1 30 | namespaced_redis.get('foo') 31 | # => nil 32 | redis_connection.get('ns:foo') 33 | # => nil 34 | ``` 35 | 36 | Redis::Namespace also supports `Proc` as a namespace and will take the result string as namespace at runtime. 37 | 38 | ```ruby 39 | redis_connection = Redis.new 40 | namespaced_redis = Redis::Namespace.new(Proc.new { Tenant.current_tenant }, redis: redis_connection) 41 | ``` 42 | 43 | Installation 44 | ============ 45 | 46 | Redis::Namespace is packaged as the redis-namespace gem, and hosted on rubygems.org. 47 | 48 | From the command line: 49 | 50 | $ gem install redis-namespace 51 | 52 | Or in your Gemfile: 53 | 54 | ```ruby 55 | gem 'redis-namespace' 56 | ``` 57 | 58 | Caveats 59 | ======= 60 | 61 | `Redis::Namespace` provides a namespaced interface to `Redis` by keeping an internal registry of the method signatures in `Redis` provided by the [redis-rb][] gem; we keep track of which arguments need the namespace added, and which return values need the namespace removed. 62 | 63 | Blind Passthrough 64 | ----------------- 65 | If your version of this gem doesn't know about a particular command, it can't namespace it. Historically, this has meant that Redis::Namespace blindly passes unknown commands on to the underlying redis connection without modification which can lead to surprising effects. 66 | 67 | As of v1.5.0, blind passthrough has been deprecated, and the functionality will be removed entirely in 2.0. 68 | 69 | If you come across a command that is not yet supported, please open an issue on the [issue tracker][] or submit a pull-request. 70 | 71 | Administrative Commands 72 | ----------------------- 73 | The effects of some redis commands cannot be limited to a particular namespace (e.g., `FLUSHALL`, which literally truncates all databases in your redis server, regardless of keyspace). Historically, this has meant that Redis::Namespace intentionally passes administrative commands on to the underlying redis connection without modification, which can lead to surprising effects. 74 | 75 | As of v1.6.0, the direct use of administrative commands has been deprecated, and the functionality will be removed entirely in 2.0; while such commands are often useful for testing or administration, their meaning is inherently hidden when placed behind an interface that implies it will namespace everything. 76 | 77 | The prefered way to send an administrative command is on the redis connection itself, which is publicly exposed as `Redis::Namespace#redis`: 78 | 79 | ```ruby 80 | namespaced.redis.flushall() 81 | # => "OK" 82 | ``` 83 | 84 | 2.x Planned Breaking Changes 85 | ============================ 86 | 87 | As mentioned above, 2.0 will remove blind passthrough and the administrative command passthrough. 88 | By default in 1.5+, deprecation warnings are present and enabled; 89 | they can be silenced by initializing `Redis::Namespace` with `warning: false` or by setting the `REDIS_NAMESPACE_QUIET` environment variable. 90 | 91 | Early opt-in 92 | ------------ 93 | 94 | To enable testing against the 2.x interface before its release, in addition to deprecation warnings, early opt-in to these changes can be enabled by initializing `Redis::Namespace` with `deprecations: true` or by setting the `REDIS_NAMESPACE_DEPRECATIONS` environment variable. 95 | This should only be done once all warnings have been addressed. 96 | 97 | Authors 98 | ======= 99 | 100 | While there are many authors who have contributed to this project, the following have done so on an ongoing basis with at least 5 commits: 101 | 102 | - Chris Wanstrath (@defunkt) 103 | - Ryan Biesemeyer (@yaauie) 104 | - Steve Klabnik (@steveklabnik) 105 | - Terence Lee (@hone) 106 | - Eoin Coffey (@ecoffey) 107 | 108 | [redis]: http://redis.io 109 | [redis-rb]: https://github.com/redis/redis-rb 110 | [issue tracker]: https://github.com/resque/redis-namespace/issues 111 | -------------------------------------------------------------------------------- /lib/redis/namespace.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | require 'redis/namespace/version' 3 | 4 | class Redis 5 | class Namespace 6 | # The following tables define how input parameters and result 7 | # values should be modified for the namespace. 8 | # 9 | # COMMANDS is a hash. Each key is the name of a command and each 10 | # value is a two element array. 11 | # 12 | # The first element in the value array describes how to modify the 13 | # arguments passed. It can be one of: 14 | # 15 | # nil 16 | # Do nothing. 17 | # :first 18 | # Add the namespace to the first argument passed, e.g. 19 | # GET key => GET namespace:key 20 | # :all 21 | # Add the namespace to all arguments passed, e.g. 22 | # MGET key1 key2 => MGET namespace:key1 namespace:key2 23 | # :exclude_first 24 | # Add the namespace to all arguments but the first, e.g. 25 | # :exclude_last 26 | # Add the namespace to all arguments but the last, e.g. 27 | # BLPOP key1 key2 timeout => 28 | # BLPOP namespace:key1 namespace:key2 timeout 29 | # :exclude_options 30 | # Add the namespace to all arguments, except the last argument, 31 | # if the last argument is a hash of options. 32 | # ZUNIONSTORE key1 2 key2 key3 WEIGHTS 2 1 => 33 | # ZUNIONSTORE namespace:key1 2 namespace:key2 namespace:key3 WEIGHTS 2 1 34 | # :alternate 35 | # Add the namespace to every other argument, e.g. 36 | # MSET key1 value1 key2 value2 => 37 | # MSET namespace:key1 value1 namespace:key2 value2 38 | # :sort 39 | # Add namespace to first argument if it is non-nil 40 | # Add namespace to second arg's :by and :store if second arg is a Hash 41 | # Add namespace to each element in second arg's :get if second arg is 42 | # a Hash; forces second arg's :get to be an Array if present. 43 | # :eval_style 44 | # Add namespace to each element in keys argument (via options hash or multi-args) 45 | # :scan_style 46 | # Add namespace to :match option, or supplies "#{namespace}:*" if not present. 47 | # 48 | # The second element in the value array describes how to modify 49 | # the return value of the Redis call. It can be one of: 50 | # 51 | # nil 52 | # Do nothing. 53 | # :all 54 | # Add the namespace to all elements returned, e.g. 55 | # key1 key2 => namespace:key1 namespace:key2 56 | NAMESPACED_COMMANDS = { 57 | "append" => [ :first ], 58 | "bitcount" => [ :first ], 59 | "bitfield" => [ :first ], 60 | "bitop" => [ :exclude_first ], 61 | "bitpos" => [ :first ], 62 | "blpop" => [ :exclude_last, :first ], 63 | "brpop" => [ :exclude_last, :first ], 64 | "brpoplpush" => [ :exclude_last ], 65 | "bzpopmin" => [ :first ], 66 | "bzpopmax" => [ :first ], 67 | "debug" => [ :exclude_first ], 68 | "decr" => [ :first ], 69 | "decrby" => [ :first ], 70 | "del" => [ :all ], 71 | "dump" => [ :first ], 72 | "exists" => [ :all ], 73 | "exists?" => [ :all ], 74 | "expire" => [ :first ], 75 | "expireat" => [ :first ], 76 | "expiretime" => [ :first ], 77 | "eval" => [ :eval_style ], 78 | "evalsha" => [ :eval_style ], 79 | "get" => [ :first ], 80 | "getex" => [ :first ], 81 | "getbit" => [ :first ], 82 | "getrange" => [ :first ], 83 | "getset" => [ :first ], 84 | "hset" => [ :first ], 85 | "hsetnx" => [ :first ], 86 | "hget" => [ :first ], 87 | "hincrby" => [ :first ], 88 | "hincrbyfloat" => [ :first ], 89 | "hmget" => [ :first ], 90 | "hmset" => [ :first ], 91 | "hdel" => [ :first ], 92 | "hexists" => [ :first ], 93 | "hlen" => [ :first ], 94 | "hkeys" => [ :first ], 95 | "hscan" => [ :first ], 96 | "hscan_each" => [ :first ], 97 | "hvals" => [ :first ], 98 | "hgetall" => [ :first ], 99 | "incr" => [ :first ], 100 | "incrby" => [ :first ], 101 | "incrbyfloat" => [ :first ], 102 | "keys" => [ :first, :all ], 103 | "lindex" => [ :first ], 104 | "linsert" => [ :first ], 105 | "llen" => [ :first ], 106 | "lpop" => [ :first ], 107 | "lpos" => [ :first ], 108 | "lpush" => [ :first ], 109 | "lpushx" => [ :first ], 110 | "lrange" => [ :first ], 111 | "lrem" => [ :first ], 112 | "lset" => [ :first ], 113 | "ltrim" => [ :first ], 114 | "mapped_hmset" => [ :first ], 115 | "mapped_hmget" => [ :first ], 116 | "mapped_mget" => [ :all, :all ], 117 | "mapped_mset" => [ :all ], 118 | "mapped_msetnx" => [ :all ], 119 | "mget" => [ :all ], 120 | "monitor" => [ :monitor ], 121 | "move" => [ :first ], 122 | "mset" => [ :alternate ], 123 | "msetnx" => [ :alternate ], 124 | "object" => [ :exclude_first ], 125 | "persist" => [ :first ], 126 | "pexpire" => [ :first ], 127 | "pexpireat" => [ :first ], 128 | "pexpiretime" => [ :first ], 129 | "pfadd" => [ :first ], 130 | "pfcount" => [ :all ], 131 | "pfmerge" => [ :all ], 132 | "psetex" => [ :first ], 133 | "psubscribe" => [ :all ], 134 | "pttl" => [ :first ], 135 | "publish" => [ :first ], 136 | "punsubscribe" => [ :all ], 137 | "rename" => [ :all ], 138 | "renamenx" => [ :all ], 139 | "restore" => [ :first ], 140 | "rpop" => [ :first ], 141 | "rpoplpush" => [ :all ], 142 | "rpush" => [ :first ], 143 | "rpushx" => [ :first ], 144 | "sadd" => [ :first ], 145 | "sadd?" => [ :first ], 146 | "scard" => [ :first ], 147 | "scan" => [ :scan_style, :second ], 148 | "scan_each" => [ :scan_style, :all ], 149 | "sdiff" => [ :all ], 150 | "sdiffstore" => [ :all ], 151 | "set" => [ :first ], 152 | "setbit" => [ :first ], 153 | "setex" => [ :first ], 154 | "setnx" => [ :first ], 155 | "setrange" => [ :first ], 156 | "sinter" => [ :all ], 157 | "sinterstore" => [ :all ], 158 | "sismember" => [ :first ], 159 | "smembers" => [ :first ], 160 | "smismember" => [ :first ], 161 | "smove" => [ :exclude_last ], 162 | "sort" => [ :sort ], 163 | "spop" => [ :first ], 164 | "srandmember" => [ :first ], 165 | "srem" => [ :first ], 166 | "srem?" => [ :first ], 167 | "sscan" => [ :first ], 168 | "sscan_each" => [ :first ], 169 | "strlen" => [ :first ], 170 | "subscribe" => [ :all ], 171 | "sunion" => [ :all ], 172 | "sunionstore" => [ :all ], 173 | "ttl" => [ :first ], 174 | "type" => [ :first ], 175 | "unlink" => [ :all ], 176 | "unsubscribe" => [ :all ], 177 | "zadd" => [ :first ], 178 | "zcard" => [ :first ], 179 | "zcount" => [ :first ], 180 | "zincrby" => [ :first ], 181 | "zinterstore" => [ :exclude_options ], 182 | "zpopmin" => [ :first ], 183 | "zpopmax" => [ :first ], 184 | "zrange" => [ :first ], 185 | "zrangebyscore" => [ :first ], 186 | "zrangebylex" => [ :first ], 187 | "zrank" => [ :first ], 188 | "zrem" => [ :first ], 189 | "zremrangebyrank" => [ :first ], 190 | "zremrangebyscore" => [ :first ], 191 | "zremrangebylex" => [ :first ], 192 | "zrevrange" => [ :first ], 193 | "zrevrangebyscore" => [ :first ], 194 | "zrevrangebylex" => [ :first ], 195 | "zrevrank" => [ :first ], 196 | "zscan" => [ :first ], 197 | "zscan_each" => [ :first ], 198 | "zscore" => [ :first ], 199 | "zunionstore" => [ :exclude_options ] 200 | } 201 | TRANSACTION_COMMANDS = { 202 | "discard" => [], 203 | "exec" => [], 204 | "multi" => [], 205 | "unwatch" => [ :all ], 206 | "watch" => [ :all ], 207 | } 208 | HELPER_COMMANDS = { 209 | "auth" => [], 210 | "disconnect!" => [], 211 | "close" => [], 212 | "echo" => [], 213 | "ping" => [], 214 | "time" => [], 215 | } 216 | ADMINISTRATIVE_COMMANDS = { 217 | "bgrewriteaof" => [], 218 | "bgsave" => [], 219 | "config" => [], 220 | "dbsize" => [], 221 | "flushall" => [], 222 | "flushdb" => [], 223 | "info" => [], 224 | "lastsave" => [], 225 | "quit" => [], 226 | "randomkey" => [], 227 | "save" => [], 228 | "script" => [], 229 | "select" => [], 230 | "shutdown" => [], 231 | "slaveof" => [], 232 | } 233 | 234 | DEPRECATED_COMMANDS = [ 235 | ADMINISTRATIVE_COMMANDS 236 | ].compact.reduce(:merge) 237 | 238 | COMMANDS = [ 239 | NAMESPACED_COMMANDS, 240 | TRANSACTION_COMMANDS, 241 | HELPER_COMMANDS, 242 | ADMINISTRATIVE_COMMANDS, 243 | ].compact.reduce(:merge) 244 | 245 | # Support 1.8.7 by providing a namespaced reference to Enumerable::Enumerator 246 | Enumerator = Enumerable::Enumerator unless defined?(::Enumerator) 247 | 248 | # This is used by the Redis gem to determine whether or not to display that deprecation message. 249 | @sadd_returns_boolean = true 250 | 251 | # This is used by the Redis gem to determine whether or not to display that deprecation message. 252 | @srem_returns_boolean = true 253 | 254 | class << self 255 | attr_accessor :sadd_returns_boolean, :srem_returns_boolean 256 | end 257 | 258 | attr_writer :namespace 259 | attr_reader :redis 260 | attr_accessor :warning 261 | 262 | def initialize(namespace, options = {}) 263 | @namespace = namespace 264 | @redis = options[:redis] || Redis.new 265 | @warning = !!options.fetch(:warning) do 266 | !ENV['REDIS_NAMESPACE_QUIET'] 267 | end 268 | @deprecations = !!options.fetch(:deprecations) do 269 | ENV['REDIS_NAMESPACE_DEPRECATIONS'] 270 | end 271 | @has_new_client_method = @redis.respond_to?(:_client) 272 | end 273 | 274 | def deprecations? 275 | @deprecations 276 | end 277 | 278 | def warning? 279 | @warning 280 | end 281 | 282 | def client 283 | warn("The client method is deprecated as of redis-rb 4.0.0, please use the new _client " + 284 | "method instead. Support for the old method will be removed in redis-namespace 2.0.") if @has_new_client_method && deprecations? 285 | _client 286 | end 287 | 288 | def _client 289 | @has_new_client_method ? @redis._client : @redis.client # for redis-4.0.0 290 | end 291 | 292 | # Ruby defines a now deprecated type method so we need to override it here 293 | # since it will never hit method_missing 294 | def type(key) 295 | call_with_namespace(:type, key) 296 | end 297 | 298 | alias_method :self_respond_to?, :respond_to? 299 | 300 | # emulate Ruby 1.9+ and keep respond_to_missing? logic together. 301 | def respond_to?(command, include_private=false) 302 | return !deprecations? if DEPRECATED_COMMANDS.include?(command.to_s.downcase) 303 | 304 | respond_to_missing?(command, include_private) or super 305 | end 306 | 307 | def keys(query = nil) 308 | call_with_namespace(:keys, query || '*') 309 | end 310 | 311 | def multi(&block) 312 | if block_given? 313 | namespaced_block(:multi, &block) 314 | else 315 | call_with_namespace(:multi) 316 | end 317 | end 318 | 319 | def pipelined(&block) 320 | namespaced_block(:pipelined, &block) 321 | end 322 | 323 | def namespace(desired_namespace = nil) 324 | if desired_namespace 325 | yield Redis::Namespace.new(desired_namespace, 326 | :redis => @redis) 327 | end 328 | 329 | @namespace.respond_to?(:call) ? @namespace.call : @namespace 330 | end 331 | 332 | def full_namespace 333 | redis.is_a?(Namespace) ? "#{redis.full_namespace}:#{namespace}" : namespace.to_s 334 | end 335 | 336 | def connection 337 | @redis.connection.tap { |info| info[:namespace] = namespace } 338 | end 339 | 340 | def exec 341 | call_with_namespace(:exec) 342 | end 343 | 344 | def eval(*args) 345 | call_with_namespace(:eval, *args) 346 | end 347 | ruby2_keywords(:eval) if respond_to?(:ruby2_keywords, true) 348 | 349 | # This operation can run for a very long time if the namespace contains lots of keys! 350 | # It should be used in tests, or when the namespace is small enough 351 | # and you are sure you know what you are doing. 352 | def clear 353 | if warning? 354 | warn("This operation can run for a very long time if the namespace contains lots of keys! " + 355 | "It should be used in tests, or when the namespace is small enough " + 356 | "and you are sure you know what you are doing.") 357 | end 358 | 359 | batch_size = 1000 360 | 361 | if supports_scan? 362 | cursor = "0" 363 | begin 364 | cursor, keys = scan(cursor, count: batch_size) 365 | del(*keys) unless keys.empty? 366 | end until cursor == "0" 367 | else 368 | all_keys = keys("*") 369 | all_keys.each_slice(batch_size) do |keys| 370 | del(*keys) 371 | end 372 | end 373 | end 374 | 375 | ADMINISTRATIVE_COMMANDS.keys.each do |command| 376 | define_method(command) do |*args, &block| 377 | raise NoMethodError if deprecations? 378 | 379 | if warning? 380 | warn("Passing '#{command}' command to redis as is; " + 381 | "administrative commands cannot be effectively namespaced " + 382 | "and should be called on the redis connection directly; " + 383 | "passthrough has been deprecated and will be removed in " + 384 | "redis-namespace 2.0 (at #{call_site})" 385 | ) 386 | end 387 | call_with_namespace(command, *args, &block) 388 | end 389 | ruby2_keywords(command) if respond_to?(:ruby2_keywords, true) 390 | end 391 | 392 | COMMANDS.keys.each do |command| 393 | next if ADMINISTRATIVE_COMMANDS.include?(command) 394 | next if method_defined?(command) 395 | 396 | define_method(command) do |*args, &block| 397 | call_with_namespace(command, *args, &block) 398 | end 399 | ruby2_keywords(command) if respond_to?(:ruby2_keywords, true) 400 | end 401 | 402 | def method_missing(command, *args, &block) 403 | normalized_command = command.to_s.downcase 404 | 405 | if COMMANDS.include?(normalized_command) 406 | send(normalized_command, *args, &block) 407 | elsif @redis.respond_to?(normalized_command) && !deprecations? 408 | # blind passthrough is deprecated and will be removed in 2.0 409 | # redis-namespace does not know how to handle this command. 410 | # Passing it to @redis as is, where redis-namespace shows 411 | # a warning message if @warning is set. 412 | if warning? 413 | warn("Passing '#{command}' command to redis as is; blind " + 414 | "passthrough has been deprecated and will be removed in " + 415 | "redis-namespace 2.0 (at #{call_site})") 416 | end 417 | 418 | wrapped_send(@redis, command, args, &block) 419 | else 420 | super 421 | end 422 | end 423 | ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true) 424 | 425 | def inspect 426 | "<#{self.class.name} v#{VERSION} with client v#{Redis::VERSION} "\ 427 | "for #{@redis.id}/#{full_namespace}>" 428 | end 429 | 430 | def respond_to_missing?(command, include_all=false) 431 | normalized_command = command.to_s.downcase 432 | 433 | case 434 | when COMMANDS.include?(normalized_command) 435 | true 436 | when !deprecations? && redis.respond_to?(command, include_all) 437 | true 438 | else 439 | defined?(super) && super 440 | end 441 | end 442 | 443 | def call_with_namespace(command, *args, &block) 444 | handling = COMMANDS[command.to_s.downcase] 445 | 446 | if handling.nil? 447 | fail("Redis::Namespace does not know how to handle '#{command}'.") 448 | end 449 | 450 | (before, after) = handling 451 | 452 | # Modify the local *args array in-place, no need to copy it. 453 | args.map! {|arg| clone_args(arg)} 454 | 455 | # Add the namespace to any parameters that are keys. 456 | case before 457 | when :first 458 | args[0] = add_namespace(args[0]) if args[0] 459 | args[-1] = ruby2_keywords_hash(args[-1]) if args[-1].is_a?(Hash) 460 | when :all 461 | args = add_namespace(args) 462 | when :exclude_first 463 | first = args.shift 464 | args = add_namespace(args) 465 | args.unshift(first) if first 466 | when :exclude_last 467 | last = args.pop unless args.length == 1 468 | args = add_namespace(args) 469 | args.push(last) if last 470 | when :exclude_options 471 | if args.last.is_a?(Hash) 472 | last = ruby2_keywords_hash(args.pop) 473 | args = add_namespace(args) 474 | args.push(last) 475 | else 476 | args = add_namespace(args) 477 | end 478 | when :alternate 479 | args = args.flatten 480 | args.each_with_index { |a, i| args[i] = add_namespace(a) if i.even? } 481 | when :sort 482 | args[0] = add_namespace(args[0]) if args[0] 483 | if args[1].is_a?(Hash) 484 | [:by, :store].each do |key| 485 | args[1][key] = add_namespace(args[1][key]) if args[1][key] 486 | end 487 | 488 | args[1][:get] = Array(args[1][:get]) 489 | 490 | args[1][:get].each_index do |i| 491 | args[1][:get][i] = add_namespace(args[1][:get][i]) unless args[1][:get][i] == "#" 492 | end 493 | args[1] = ruby2_keywords_hash(args[1]) 494 | end 495 | when :eval_style 496 | # redis.eval() and evalsha() can either take the form: 497 | # 498 | # redis.eval(script, [key1, key2], [argv1, argv2]) 499 | # 500 | # Or: 501 | # 502 | # redis.eval(script, :keys => ['k1', 'k2'], :argv => ['arg1', 'arg2']) 503 | # 504 | # This is a tricky + annoying special case, where we only want the `keys` 505 | # argument to be namespaced. 506 | if args.last.is_a?(Hash) 507 | args.last[:keys] = add_namespace(args.last[:keys]) 508 | else 509 | args[1] = add_namespace(args[1]) 510 | end 511 | when :scan_style 512 | options = (args.last.kind_of?(Hash) ? args.pop : {}) 513 | options[:match] = add_namespace(options.fetch(:match, '*')) 514 | args << ruby2_keywords_hash(options) 515 | 516 | if block 517 | original_block = block 518 | block = proc { |key| original_block.call rem_namespace(key) } 519 | end 520 | end 521 | 522 | # Dispatch the command to Redis and store the result. 523 | result = wrapped_send(@redis, command, args, &block) 524 | 525 | # Don't try to remove namespace from a Redis::Future, you can't. 526 | return result if result.is_a?(Redis::Future) 527 | 528 | # Remove the namespace from results that are keys. 529 | case after 530 | when :all 531 | result = rem_namespace(result) 532 | when :first 533 | result[0] = rem_namespace(result[0]) if result 534 | when :second 535 | result[1] = rem_namespace(result[1]) if result 536 | end 537 | 538 | result 539 | end 540 | ruby2_keywords(:call_with_namespace) if respond_to?(:ruby2_keywords, true) 541 | 542 | protected 543 | 544 | def redis=(redis) 545 | @redis = redis 546 | end 547 | 548 | private 549 | 550 | if Hash.respond_to?(:ruby2_keywords_hash) 551 | def ruby2_keywords_hash(kwargs) 552 | Hash.ruby2_keywords_hash(kwargs) 553 | end 554 | else 555 | def ruby2_keywords_hash(kwargs) 556 | kwargs 557 | end 558 | end 559 | 560 | def wrapped_send(redis_client, command, args = [], &block) 561 | if redis_client.class.name == "ConnectionPool" 562 | redis_client.with do |pool_connection| 563 | pool_connection.send(command, *args, &block) 564 | end 565 | else 566 | redis_client.send(command, *args, &block) 567 | end 568 | end 569 | 570 | # Avoid modifying the caller's (pass-by-reference) arguments. 571 | def clone_args(arg) 572 | if arg.is_a?(Array) 573 | arg.map {|sub_arg| clone_args(sub_arg)} 574 | elsif arg.is_a?(Hash) 575 | Hash[arg.map {|k, v| [clone_args(k), clone_args(v)]}] 576 | else 577 | arg # Some objects (e.g. symbol) can't be dup'd. 578 | end 579 | end 580 | 581 | def call_site 582 | caller.reject { |l| l.start_with?(__FILE__) }.first 583 | end 584 | 585 | def namespaced_block(command, &block) 586 | if block.arity == 0 587 | wrapped_send(redis, command, &block) 588 | else 589 | outer_block = proc { |r| copy = dup; copy.redis = r; yield copy } 590 | wrapped_send(redis, command, &outer_block) 591 | end 592 | end 593 | 594 | def add_namespace(key) 595 | return key unless key && namespace 596 | 597 | case key 598 | when Array 599 | key.map! {|k| add_namespace k} 600 | when Hash 601 | key.keys.each {|k| key[add_namespace(k)] = key.delete(k)} 602 | key 603 | else 604 | "#{namespace}:#{key}" 605 | end 606 | end 607 | 608 | def rem_namespace(key) 609 | return key unless key && namespace 610 | 611 | case key 612 | when Array 613 | key.map {|k| rem_namespace k} 614 | when Hash 615 | Hash[*key.map {|k, v| [ rem_namespace(k), v ]}.flatten] 616 | when Enumerator 617 | create_enumerator do |yielder| 618 | key.each { |k| yielder.yield rem_namespace(k) } 619 | end 620 | else 621 | key.to_s.sub(/\A#{namespace}:/, '') 622 | end 623 | end 624 | 625 | def create_enumerator(&block) 626 | # Enumerator in 1.8.7 *requires* a single argument, so we need to use 627 | # its Generator class, which matches the block syntax of 1.9.x's 628 | # Enumerator class. 629 | if RUBY_VERSION.start_with?('1.8') 630 | require 'generator' unless defined?(Generator) 631 | Generator.new(&block).to_enum 632 | else 633 | Enumerator.new(&block) 634 | end 635 | end 636 | 637 | def supports_scan? 638 | redis_version = @redis.info["redis_version"] 639 | Gem::Version.new(redis_version) >= Gem::Version.new("2.8.0") 640 | end 641 | end 642 | end 643 | -------------------------------------------------------------------------------- /spec/redis_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require File.dirname(__FILE__) + '/spec_helper' 4 | require 'connection_pool' 5 | 6 | describe "redis" do 7 | @redis_version = Gem::Version.new(Redis.new.info["redis_version"]) 8 | let(:redis_client) { @redis.respond_to?(:_client) ? @redis._client : @redis.client} 9 | 10 | before(:each) do 11 | # use database 15 for testing so we dont accidentally step on your real data 12 | @redis = Redis.new :db => 15 13 | @redis.flushdb 14 | @namespaced = Redis::Namespace.new(:ns, :redis => @redis) 15 | @redis.set('foo', 'bar') 16 | end 17 | 18 | # redis-rb 3.3.4+ 19 | it "should inject :namespace into connection info" do 20 | info = @redis.connection.merge(:namespace => :ns) 21 | expect(@namespaced.connection).to eq(info) 22 | end 23 | 24 | it "proxies `client` to the _client and deprecated" do 25 | expect(@namespaced.client).to eq(redis_client) 26 | end 27 | 28 | it "proxies `_client` to the _client" do 29 | expect(@namespaced._client).to eq(redis_client) 30 | end 31 | 32 | it "should be able to use a namespace" do 33 | expect(@namespaced.get('foo')).to eq(nil) 34 | @namespaced.set('foo', 'chris') 35 | expect(@namespaced.get('foo')).to eq('chris') 36 | @redis.set('foo', 'bob') 37 | expect(@redis.get('foo')).to eq('bob') 38 | 39 | @namespaced.incrby('counter', 2) 40 | expect(@namespaced.get('counter').to_i).to eq(2) 41 | expect(@redis.get('counter')).to eq(nil) 42 | expect(@namespaced.type('counter')).to eq('string') 43 | end 44 | 45 | it "should work with Proc namespaces" do 46 | namespace = Proc.new { :dynamic_ns } 47 | namespaced = Redis::Namespace.new(namespace, redis: @redis) 48 | 49 | expect(namespaced.get('foo')).to eq(nil) 50 | namespaced.set('foo', 'chris') 51 | expect(namespaced.get('foo')).to eq('chris') 52 | @redis.set('foo', 'bob') 53 | expect(@redis.get('foo')).to eq('bob') 54 | end 55 | 56 | context 'when sending capital commands (issue 68)' do 57 | it 'should be able to use a namespace' do 58 | @namespaced.send('SET', 'fubar', 'quux') 59 | expect(@redis.get('fubar')).to be_nil 60 | expect(@namespaced.get('fubar')).to eq 'quux' 61 | end 62 | end 63 | 64 | it "should be able to use a namespace with bpop" do 65 | @namespaced.rpush "foo", "string" 66 | @namespaced.rpush "foo", "ns:string" 67 | @namespaced.rpush "foo", "string_no_timeout" 68 | expect(@namespaced.blpop("foo", 1)).to eq(["foo", "string"]) 69 | expect(@namespaced.blpop("foo", 1)).to eq(["foo", "ns:string"]) 70 | expect(@namespaced.blpop("foo")).to eq(["foo", "string_no_timeout"]) 71 | expect(@namespaced.blpop("foo", 1)).to eq(nil) 72 | end 73 | 74 | it "should be able to use a namespace with del" do 75 | @namespaced.set('foo', 1000) 76 | @namespaced.set('bar', 2000) 77 | @namespaced.set('baz', 3000) 78 | @namespaced.del 'foo' 79 | expect(@namespaced.get('foo')).to eq(nil) 80 | @namespaced.del 'bar', 'baz' 81 | expect(@namespaced.get('bar')).to eq(nil) 82 | expect(@namespaced.get('baz')).to eq(nil) 83 | end 84 | 85 | it "should be able to use a namespace with unlink" do 86 | @namespaced.set('foo', 1000) 87 | @namespaced.set('bar', 2000) 88 | @namespaced.set('baz', 3000) 89 | @namespaced.unlink 'foo' 90 | expect(@namespaced.get('foo')).to eq(nil) 91 | @namespaced.unlink 'bar', 'baz' 92 | expect(@namespaced.get('bar')).to eq(nil) 93 | expect(@namespaced.get('baz')).to eq(nil) 94 | end 95 | 96 | it 'should be able to use a namespace with append' do 97 | @namespaced.set('foo', 'bar') 98 | expect(@namespaced.append('foo','n')).to eq(4) 99 | expect(@namespaced.get('foo')).to eq('barn') 100 | expect(@redis.get('foo')).to eq('bar') 101 | end 102 | 103 | it 'should be able to use a namespace with lpos' do 104 | @namespaced.rpush('foo', %w[a b c 1 2 3 c c]) 105 | expect(@namespaced.lpos('foo', 'c')).to eq(2) 106 | expect(@redis.lpos('mykey', 'c')).to be_nil 107 | end 108 | 109 | it 'should be able to use a namespace with brpoplpush' do 110 | @namespaced.lpush('foo','bar') 111 | expect(@namespaced.brpoplpush('foo','bar',0)).to eq('bar') 112 | expect(@namespaced.lrange('foo',0,-1)).to eq([]) 113 | expect(@namespaced.lrange('bar',0,-1)).to eq(['bar']) 114 | end 115 | 116 | it "should be able to use a namespace with getex" do 117 | expect(@namespaced.set('mykey', 'Hello')).to eq('OK') 118 | expect(@namespaced.getex('mykey', ex: 50)).to eq('Hello') 119 | expect(@namespaced.get('mykey')).to eq('Hello') 120 | expect(@namespaced.ttl('mykey')).to eq(50) 121 | end 122 | 123 | it 'should be able to use a namespace with getbit' do 124 | @namespaced.set('foo','bar') 125 | expect(@namespaced.getbit('foo',1)).to eq(1) 126 | end 127 | 128 | it 'should be able to use a namespace with getrange' do 129 | @namespaced.set('foo','bar') 130 | expect(@namespaced.getrange('foo',0,-1)).to eq('bar') 131 | end 132 | 133 | it 'should be able to use a namespace with linsert' do 134 | @namespaced.rpush('foo','bar') 135 | @namespaced.rpush('foo','barn') 136 | @namespaced.rpush('foo','bart') 137 | expect(@namespaced.linsert('foo','BEFORE','barn','barf')).to eq(4) 138 | expect(@namespaced.lrange('foo',0,-1)).to eq(['bar','barf','barn','bart']) 139 | end 140 | 141 | it 'should be able to use a namespace with lpushx' do 142 | expect(@namespaced.lpushx('foo','bar')).to eq(0) 143 | @namespaced.lpush('foo','boo') 144 | expect(@namespaced.lpushx('foo','bar')).to eq(2) 145 | expect(@namespaced.lrange('foo',0,-1)).to eq(['bar','boo']) 146 | end 147 | 148 | it 'should be able to use a namespace with rpushx' do 149 | expect(@namespaced.rpushx('foo','bar')).to eq(0) 150 | @namespaced.lpush('foo','boo') 151 | expect(@namespaced.rpushx('foo','bar')).to eq(2) 152 | expect(@namespaced.lrange('foo',0,-1)).to eq(['boo','bar']) 153 | end 154 | 155 | it 'should be able to use a namespace with setbit' do 156 | @namespaced.setbit('virgin_key', 1, 1) 157 | expect(@namespaced.exists?('virgin_key')).to be true 158 | expect(@namespaced.get('virgin_key')).to eq(@namespaced.getrange('virgin_key',0,-1)) 159 | end 160 | 161 | it 'should be able to use a namespace with exists' do 162 | @namespaced.set('foo', 1000) 163 | @namespaced.set('bar', 2000) 164 | expect(@namespaced.exists('foo', 'bar')).to eq(2) 165 | end 166 | 167 | it 'should be able to use a namespace with exists?' do 168 | @namespaced.set('foo', 1000) 169 | @namespaced.set('bar', 2000) 170 | expect(@namespaced.exists?('does_not_exist', 'bar')).to eq(true) 171 | end 172 | 173 | it 'should be able to use a namespace with bitpos' do 174 | @namespaced.setbit('bit_map', 42, 1) 175 | expect(@namespaced.bitpos('bit_map', 0)).to eq(0) 176 | expect(@namespaced.bitpos('bit_map', 1)).to eq(42) 177 | end 178 | 179 | it 'should be able to use a namespace with setrange' do 180 | @namespaced.setrange('foo', 0, 'bar') 181 | expect(@namespaced.get('foo')).to eq('bar') 182 | 183 | @namespaced.setrange('bar', 2, 'foo') 184 | expect(@namespaced.get('bar')).to eq("\000\000foo") 185 | end 186 | 187 | it "should be able to use a namespace with mget" do 188 | @namespaced.set('foo', 1000) 189 | @namespaced.set('bar', 2000) 190 | expect(@namespaced.mapped_mget('foo', 'bar')).to eq({ 'foo' => '1000', 'bar' => '2000' }) 191 | expect(@namespaced.mapped_mget('foo', 'baz', 'bar')).to eq({'foo'=>'1000', 'bar'=>'2000', 'baz' => nil}) 192 | end 193 | 194 | it "should utilize connection_pool while using a namespace with mget" do 195 | memo = @namespaced 196 | connection_pool = ConnectionPool.new(size: 2, timeout: 2) { Redis.new db: 15 } 197 | @namespaced = Redis::Namespace.new(:ns, redis: connection_pool) 198 | 199 | expect(connection_pool).to receive(:with).and_call_original do |arg| 200 | expect(arg).to be(an_instance_of(Redis)) 201 | end.at_least(:once) 202 | 203 | @namespaced.set('foo', 1000) 204 | @namespaced.set('bar', 2000) 205 | expect(@namespaced.mapped_mget('foo', 'bar')).to eq({ 'foo' => '1000', 'bar' => '2000' }) 206 | expect(@namespaced.mapped_mget('foo', 'baz', 'bar')).to eq({'foo'=>'1000', 'bar'=>'2000', 'baz' => nil}) 207 | @redis.get('foo').should eq('bar') 208 | 209 | @namespaced = memo 210 | end 211 | 212 | it "should be able to use a namespace with mset" do 213 | @namespaced.mset('foo', '1000', 'bar', '2000') 214 | expect(@namespaced.mapped_mget('foo', 'bar')).to eq({ 'foo' => '1000', 'bar' => '2000' }) 215 | expect(@namespaced.mapped_mget('foo', 'baz', 'bar')).to eq({ 'foo' => '1000', 'bar' => '2000', 'baz' => nil}) 216 | 217 | @namespaced.mapped_mset('foo' => '3000', 'bar' => '5000') 218 | expect(@namespaced.mapped_mget('foo', 'bar')).to eq({ 'foo' => '3000', 'bar' => '5000' }) 219 | expect(@namespaced.mapped_mget('foo', 'baz', 'bar')).to eq({ 'foo' => '3000', 'bar' => '5000', 'baz' => nil}) 220 | 221 | @namespaced.mset(['foo', '4000'], ['baz', '6000']) 222 | expect(@namespaced.mapped_mget('foo', 'bar', 'baz')).to eq({ 'foo' => '4000', 'bar' => '5000', 'baz' => '6000' }) 223 | end 224 | 225 | it "should be able to use a namespace with msetnx" do 226 | @namespaced.msetnx('foo', '1000', 'bar', '2000') 227 | expect(@namespaced.mapped_mget('foo', 'bar')).to eq({ 'foo' => '1000', 'bar' => '2000' }) 228 | expect(@namespaced.mapped_mget('foo', 'baz', 'bar')).to eq({ 'foo' => '1000', 'bar' => '2000', 'baz' => nil}) 229 | 230 | @namespaced.msetnx(['baz', '4000']) 231 | expect(@namespaced.mapped_mget('foo', 'baz', 'bar')).to eq({ 'foo' => '1000', 'bar' => '2000', 'baz' => '4000'}) 232 | end 233 | 234 | it "should be able to use a namespace with mapped_msetnx" do 235 | @namespaced.set('foo','1') 236 | expect(@namespaced.mapped_msetnx('foo'=>'1000', 'bar'=>'2000')).to be false 237 | expect(@namespaced.mapped_mget('foo', 'bar')).to eq({ 'foo' => '1', 'bar' => nil }) 238 | expect(@namespaced.mapped_msetnx('bar'=>'2000', 'baz'=>'1000')).to be true 239 | expect(@namespaced.mapped_mget('foo', 'bar')).to eq({ 'foo' => '1', 'bar' => '2000' }) 240 | end 241 | 242 | it "should be able to use a namespace with hashes" do 243 | @namespaced.hset('foo', 'key', 'value') 244 | @namespaced.hset('foo', 'key1', 'value1') 245 | expect(@namespaced.hget('foo', 'key')).to eq('value') 246 | expect(@namespaced.hgetall('foo')).to eq({'key' => 'value', 'key1' => 'value1'}) 247 | expect(@namespaced.hlen('foo')).to eq(2) 248 | expect(@namespaced.hkeys('foo')).to eq(['key', 'key1']) 249 | @namespaced.hmset('bar', 'key', 'value', 'key1', 'value1') 250 | @namespaced.hmget('bar', 'key', 'key1') 251 | @namespaced.hmset('bar', 'a_number', 1) 252 | expect(@namespaced.hmget('bar', 'a_number')).to eq(['1']) 253 | @namespaced.hincrby('bar', 'a_number', 3) 254 | expect(@namespaced.hmget('bar', 'a_number')).to eq(['4']) 255 | expect(@namespaced.hgetall('bar')).to eq({'key' => 'value', 'key1' => 'value1', 'a_number' => '4'}) 256 | 257 | expect(@namespaced.hsetnx('foonx','nx',10)).to be true 258 | expect(@namespaced.hsetnx('foonx','nx',12)).to be false 259 | expect(@namespaced.hget('foonx','nx')).to eq("10") 260 | expect(@namespaced.hkeys('foonx')).to eq(%w{ nx }) 261 | expect(@namespaced.hvals('foonx')).to eq(%w{ 10 }) 262 | @namespaced.mapped_hmset('baz', {'key' => 'value', 'key1' => 'value1', 'a_number' => 4}) 263 | expect(@namespaced.mapped_hmget('baz', 'key', 'key1', 'a_number')).to eq({'key' => 'value', 'key1' => 'value1', 'a_number' => '4'}) 264 | expect(@namespaced.hgetall('baz')).to eq({'key' => 'value', 'key1' => 'value1', 'a_number' => '4'}) 265 | end 266 | 267 | it "should properly intersect three sets" do 268 | @namespaced.sadd('foo', 1) 269 | @namespaced.sadd('foo', 2) 270 | @namespaced.sadd('foo', 3) 271 | @namespaced.sadd('bar', 2) 272 | @namespaced.sadd('bar', 3) 273 | @namespaced.sadd('bar', 4) 274 | @namespaced.sadd('baz', 3) 275 | expect(@namespaced.sinter('foo', 'bar', 'baz')).to eq(%w( 3 )) 276 | end 277 | 278 | it "should properly union two sets" do 279 | @namespaced.sadd('foo', 1) 280 | @namespaced.sadd('foo', 2) 281 | @namespaced.sadd('bar', 2) 282 | @namespaced.sadd('bar', 3) 283 | @namespaced.sadd('bar', 4) 284 | expect(@namespaced.sunion('foo', 'bar').sort).to eq(%w( 1 2 3 4 )) 285 | end 286 | 287 | it "should properly union two sorted sets with options" do 288 | @namespaced.zadd('sort1', 1, 1) 289 | @namespaced.zadd('sort1', 2, 2) 290 | @namespaced.zadd('sort2', 2, 2) 291 | @namespaced.zadd('sort2', 3, 3) 292 | @namespaced.zadd('sort2', 4, 4) 293 | @namespaced.zunionstore('union', ['sort1', 'sort2'], weights: [2, 1]) 294 | expect(@namespaced.zrevrange('union', 0, -1)).to eq(%w( 2 4 3 1 )) 295 | end 296 | 297 | it "should properly union two sorted sets without options" do 298 | @namespaced.zadd('sort1', 1, 1) 299 | @namespaced.zadd('sort1', 2, 2) 300 | @namespaced.zadd('sort2', 2, 2) 301 | @namespaced.zadd('sort2', 3, 3) 302 | @namespaced.zadd('sort2', 4, 4) 303 | @namespaced.zunionstore('union', ['sort1', 'sort2']) 304 | expect(@namespaced.zrevrange('union', 0, -1)).to eq(%w( 4 2 3 1 )) 305 | end 306 | 307 | it "should properly intersect two sorted sets without options" do 308 | @namespaced.zadd('food', 1, 'orange') 309 | @namespaced.zadd('food', 2, 'banana') 310 | @namespaced.zadd('food', 3, 'eggplant') 311 | 312 | @namespaced.zadd('color', 2, 'orange') 313 | @namespaced.zadd('color', 3, 'yellow') 314 | @namespaced.zadd('color', 4, 'eggplant') 315 | 316 | @namespaced.zinterstore('inter', ['food', 'color']) 317 | 318 | inter_values = @namespaced.zrevrange('inter', 0, -1, :with_scores => true) 319 | expect(inter_values).to match_array([['orange', 3.0], ['eggplant', 7.0]]) 320 | end 321 | 322 | it "should properly intersect two sorted sets with options" do 323 | @namespaced.zadd('food', 1, 'orange') 324 | @namespaced.zadd('food', 2, 'banana') 325 | @namespaced.zadd('food', 3, 'eggplant') 326 | 327 | @namespaced.zadd('color', 2, 'orange') 328 | @namespaced.zadd('color', 3, 'yellow') 329 | @namespaced.zadd('color', 4, 'eggplant') 330 | 331 | @namespaced.zinterstore('inter', ['food', 'color'], :aggregate => "min") 332 | 333 | inter_values = @namespaced.zrevrange('inter', 0, -1, :with_scores => true) 334 | expect(inter_values).to match_array([['orange', 1.0], ['eggplant', 3.0]]) 335 | end 336 | 337 | it "should return lexicographical range for sorted set" do 338 | @namespaced.zadd('food', 0, 'orange') 339 | @namespaced.zadd('food', 0, 'banana') 340 | @namespaced.zadd('food', 0, 'eggplant') 341 | 342 | values = @namespaced.zrangebylex('food', '[b', '(o') 343 | expect(values).to match_array(['banana', 'eggplant']) 344 | end 345 | 346 | it "should return the number of elements removed from the set" do 347 | @namespaced.zadd('food', 0, 'orange') 348 | @namespaced.zadd('food', 0, 'banana') 349 | @namespaced.zadd('food', 0, 'eggplant') 350 | 351 | removed = @namespaced.zremrangebylex('food', '[b', '(o') 352 | expect(removed).to eq(2) 353 | 354 | values = @namespaced.zrange('food', 0, -1) 355 | expect(values).to eq(['orange']) 356 | end 357 | 358 | it "should return reverce lexicographical range for sorted set" do 359 | @namespaced.zadd('food', 0, 'orange') 360 | @namespaced.zadd('food', 0, 'banana') 361 | @namespaced.zadd('food', 0, 'eggplant') 362 | 363 | values = @namespaced.zrevrangebylex('food', '(o', '[b') 364 | expect(values).to match_array(['banana', 'eggplant']) 365 | end 366 | 367 | it "should add a new member" do 368 | expect(@namespaced.sadd?('foo', 1)).to eq(true) 369 | expect(@namespaced.sadd?('foo', 1)).to eq(false) 370 | end 371 | 372 | it "should remove members" do 373 | @namespaced.sadd('foo', 1) 374 | expect(@namespaced.srem?('foo', 1)).to eq(true) 375 | expect(@namespaced.srem?('foo', 1)).to eq(false) 376 | end 377 | 378 | it "should add namespace to sort" do 379 | @namespaced.sadd('foo', 1) 380 | @namespaced.sadd('foo', 2) 381 | @namespaced.set('weight_1', 2) 382 | @namespaced.set('weight_2', 1) 383 | @namespaced.set('value_1', 'a') 384 | @namespaced.set('value_2', 'b') 385 | 386 | expect(@namespaced.sort('foo')).to eq(%w( 1 2 )) 387 | expect(@namespaced.sort('foo', :limit => [0, 1])).to eq(%w( 1 )) 388 | expect(@namespaced.sort('foo', :order => 'desc')).to eq(%w( 2 1 )) 389 | expect(@namespaced.sort('foo', :by => 'weight_*')).to eq(%w( 2 1 )) 390 | expect(@namespaced.sort('foo', :get => 'value_*')).to eq(%w( a b )) 391 | expect(@namespaced.sort('foo', :get => '#')).to eq(%w( 1 2 )) 392 | expect(@namespaced.sort('foo', :get => ['#', 'value_*'])).to eq([["1", "a"], ["2", "b"]]) 393 | 394 | @namespaced.sort('foo', :store => 'result') 395 | expect(@namespaced.lrange('result', 0, -1)).to eq(%w( 1 2 )) 396 | end 397 | 398 | it "should yield the correct list of keys" do 399 | @namespaced.set("foo", 1) 400 | @namespaced.set("bar", 2) 401 | @namespaced.set("baz", 3) 402 | expect(@namespaced.keys("*").sort).to eq(%w( bar baz foo )) 403 | expect(@namespaced.keys.sort).to eq(%w( bar baz foo )) 404 | end 405 | 406 | it "should add namepsace to multi blocks" do 407 | @namespaced.mapped_hmset "foo", {"key" => "value"} 408 | @namespaced.multi do |r| 409 | r.del "foo" 410 | r.mapped_hmset "foo", {"key1" => "value1"} 411 | end 412 | expect(@namespaced.hgetall("foo")).to eq({"key1" => "value1"}) 413 | end 414 | 415 | it "should utilize connection_pool while adding namepsace to multi blocks" do 416 | memo = @namespaced 417 | connection_pool = ConnectionPool.new(size: 2, timeout: 2) { Redis.new db: 15 } 418 | @namespaced = Redis::Namespace.new(:ns, redis: connection_pool) 419 | 420 | expect(connection_pool).to receive(:with).and_call_original do |arg| 421 | expect(arg).to be(an_instance_of(Redis)) 422 | end.at_least(:once) 423 | 424 | @namespaced.mapped_hmset "foo", {"key" => "value"} 425 | @namespaced.multi do |r| 426 | r.del "foo" 427 | r.mapped_hmset "foo", {"key1" => "value1"} 428 | end 429 | expect(@redis.get("foo")).to eq("bar") 430 | expect(@namespaced.hgetall("foo")).to eq({"key1" => "value1"}) 431 | 432 | @namespaced = memo 433 | end 434 | 435 | it "should pass through multi commands without block" do 436 | @namespaced.mapped_hmset "foo", {"key" => "value"} 437 | 438 | @namespaced.multi 439 | @namespaced.del "foo" 440 | @namespaced.mapped_hmset "foo", {"key1" => "value1"} 441 | @namespaced.exec 442 | 443 | expect(@namespaced.hgetall("foo")).to eq({"key1" => "value1"}) 444 | end 445 | 446 | it "should utilize connection_pool while passing through multi commands without block" do 447 | memo = @namespaced 448 | connection_pool = ConnectionPool.new(size: 2, timeout: 2) { Redis.new db: 15 } 449 | @namespaced = Redis::Namespace.new(:ns, redis: connection_pool) 450 | 451 | expect(connection_pool).to receive(:with).and_call_original do |arg| 452 | expect(arg).to be(an_instance_of(Redis)) 453 | end.at_least(:once) 454 | 455 | @namespaced.mapped_hmset "foo", {"key" => "value"} 456 | 457 | @namespaced.multi 458 | @namespaced.del "foo" 459 | @namespaced.mapped_hmset "foo", {"key1" => "value1"} 460 | @namespaced.exec 461 | 462 | expect(@namespaced.hgetall("foo")).to eq({"key1" => "value1"}) 463 | expect(@redis.get("foo")).to eq("bar") 464 | 465 | @namespaced = memo 466 | end 467 | 468 | it 'should return futures without attempting to remove namespaces' do 469 | @namespaced.multi do 470 | @future = @namespaced.keys('*') 471 | end 472 | expect(@future.class).to be(Redis::Future) 473 | end 474 | 475 | it "should add namespace to pipelined blocks" do 476 | @namespaced.mapped_hmset "foo", {"key" => "value"} 477 | @namespaced.pipelined do |r| 478 | r.del "foo" 479 | r.mapped_hmset "foo", {"key1" => "value1"} 480 | end 481 | expect(@namespaced.hgetall("foo")).to eq({"key1" => "value1"}) 482 | end 483 | 484 | it "should utilize connection_pool while adding namespace to pipelined blocks" do 485 | memo = @namespaced 486 | connection_pool = ConnectionPool.new(size: 2, timeout: 2) { Redis.new db: 15 } 487 | @namespaced = Redis::Namespace.new(:ns, redis: connection_pool) 488 | 489 | expect(connection_pool).to receive(:with).and_call_original do |arg| 490 | expect(arg).to be(an_instance_of(Redis)) 491 | end.at_least(:once) 492 | 493 | @namespaced.mapped_hmset "foo", {"key" => "value"} 494 | @namespaced.pipelined do |r| 495 | r.del "foo" 496 | r.mapped_hmset "foo", {"key1" => "value1"} 497 | end 498 | expect(@namespaced.hgetall("foo")).to eq({"key1" => "value1"}) 499 | expect(@redis.get("foo")).to eq("bar") 500 | 501 | @namespaced = memo 502 | end 503 | 504 | it "should returned response array from pipelined block" do 505 | @namespaced.mset "foo", "bar", "key", "value" 506 | result = @namespaced.pipelined do |r| 507 | r.get("foo") 508 | r.get("key") 509 | end 510 | expect(result).to eq(["bar", "value"]) 511 | end 512 | 513 | it "is thread safe for multi blocks" do 514 | mon = Monitor.new 515 | entered = false 516 | entered_cond = mon.new_cond 517 | 518 | thread = Thread.new do 519 | mon.synchronize do 520 | entered_cond.wait_until { entered } 521 | @namespaced.multi 522 | end 523 | end 524 | 525 | @namespaced.multi do |transaction| 526 | entered = true 527 | mon.synchronize { entered_cond.signal } 528 | thread.join(0.1) 529 | transaction.get("foo") 530 | end 531 | thread.join 532 | end 533 | 534 | it "should add namespace to strlen" do 535 | @namespaced.set("mykey", "123456") 536 | expect(@namespaced.strlen("mykey")).to eq(6) 537 | end 538 | 539 | it "should not add namespace to echo" do 540 | expect(@namespaced.echo(123)).to eq("123") 541 | end 542 | 543 | it 'should not add namespace to disconnect!' do 544 | expect(@redis).to receive(:disconnect!).with(no_args).and_call_original 545 | 546 | expect(@namespaced.disconnect!).to be nil 547 | end 548 | 549 | it "can change its namespace" do 550 | expect(@namespaced.get('foo')).to eq(nil) 551 | @namespaced.set('foo', 'chris') 552 | expect(@namespaced.get('foo')).to eq('chris') 553 | 554 | expect(@namespaced.namespace).to eq(:ns) 555 | @namespaced.namespace = :spec 556 | expect(@namespaced.namespace).to eq(:spec) 557 | 558 | expect(@namespaced.get('foo')).to eq(nil) 559 | @namespaced.set('foo', 'chris') 560 | expect(@namespaced.get('foo')).to eq('chris') 561 | end 562 | 563 | it "can accept a temporary namespace" do 564 | expect(@namespaced.namespace).to eq(:ns) 565 | expect(@namespaced.get('foo')).to eq(nil) 566 | 567 | @namespaced.namespace(:spec) do |temp_ns| 568 | expect(temp_ns.namespace).to eq(:spec) 569 | expect(temp_ns.get('foo')).to eq(nil) 570 | temp_ns.set('foo', 'jake') 571 | expect(temp_ns.get('foo')).to eq('jake') 572 | end 573 | 574 | expect(@namespaced.namespace).to eq(:ns) 575 | expect(@namespaced.get('foo')).to eq(nil) 576 | end 577 | 578 | it "should respond to :namespace=" do 579 | expect(@namespaced.respond_to?(:namespace=)).to eq(true) 580 | end 581 | 582 | it "should respond to :warning=" do 583 | expect(@namespaced.respond_to?(:warning=)).to eq(true) 584 | end 585 | 586 | it "should raise an exception when an unknown command is passed" do 587 | expect { @namespaced.unknown('foo') }.to raise_exception NoMethodError 588 | end 589 | 590 | describe '#inspect' do 591 | let(:single_level_names) { %i[first] } 592 | let(:double_level_names) { %i[first second] } 593 | let(:triple_level_names) { %i[first second third] } 594 | let(:namespace_builder) do 595 | ->(redis, *namespaces) { namespaces.reduce(redis) { |r, n| Redis::Namespace.new(n, redis: r) } } 596 | end 597 | let(:regexp_builder) do 598 | ->(*namespaces) { %r{/#{namespaces.join(':')}>\z} } 599 | end 600 | 601 | context 'when one namespace' do 602 | let(:single_namespaced) { namespace_builder.call(@redis, *single_level_names) } 603 | let(:regexp) { regexp_builder.call(*single_level_names) } 604 | 605 | it 'should have correct ending of inspect string' do 606 | expect(regexp =~ single_namespaced.inspect).not_to be(nil) 607 | end 608 | end 609 | 610 | context 'when two namespaces' do 611 | let(:double_namespaced) { namespace_builder.call(@redis, *double_level_names) } 612 | let(:regexp) { regexp_builder.call(*double_level_names) } 613 | 614 | it 'should have correct ending of inspect string' do 615 | expect(regexp =~ double_namespaced.inspect).not_to be(nil) 616 | end 617 | end 618 | 619 | context 'when three namespaces' do 620 | let(:triple_namespaced) { namespace_builder.call(@redis, *triple_level_names) } 621 | let(:regexp) { regexp_builder.call(*triple_level_names) } 622 | 623 | it 'should have correct ending of inspect string' do 624 | expect(regexp =~ triple_namespaced.inspect).not_to be(nil) 625 | end 626 | end 627 | end 628 | 629 | # Redis 2.6 RC reports its version as 2.5. 630 | if @redis_version >= Gem::Version.new("2.5.0") 631 | describe "redis 2.6 commands" do 632 | it "should namespace bitcount" do 633 | @redis.set('ns:foo', 'foobar') 634 | expect(@namespaced.bitcount('foo')).to eq 26 635 | expect(@namespaced.bitcount('foo', 0, 0)).to eq 4 636 | expect(@namespaced.bitcount('foo', 1, 1)).to eq 6 637 | expect(@namespaced.bitcount('foo', 3, 5)).to eq 10 638 | end 639 | 640 | it "should namespace bitop" do 641 | try_encoding('UTF-8') do 642 | @redis.set("ns:foo", "a") 643 | @redis.set("ns:bar", "b") 644 | 645 | @namespaced.bitop(:and, "foo&bar", "foo", "bar") 646 | @namespaced.bitop(:or, "foo|bar", "foo", "bar") 647 | @namespaced.bitop(:xor, "foo^bar", "foo", "bar") 648 | @namespaced.bitop(:not, "~foo", "foo") 649 | 650 | expect(@redis.get("ns:foo&bar")).to eq "\x60" 651 | expect(@redis.get("ns:foo|bar")).to eq "\x63" 652 | expect(@redis.get("ns:foo^bar")).to eq "\x03" 653 | expect(@redis.get("ns:~foo")).to eq "\x9E" 654 | end 655 | end 656 | 657 | it "should namespace dump and restore" do 658 | @redis.set("ns:foo", "a") 659 | v = @namespaced.dump("foo") 660 | @redis.del("ns:foo") 661 | 662 | expect(@namespaced.restore("foo", 1000, v)).to be_truthy 663 | expect(@redis.get("ns:foo")).to eq 'a' 664 | expect(@redis.ttl("ns:foo")).to satisfy {|v| (0..1).include?(v) } 665 | 666 | @redis.rpush("ns:bar", %w(b c d)) 667 | w = @namespaced.dump("bar") 668 | @redis.del("ns:bar") 669 | 670 | expect(@namespaced.restore("bar", 1000, w)).to be_truthy 671 | expect(@redis.lrange('ns:bar', 0, -1)).to eq %w(b c d) 672 | expect(@redis.ttl("ns:foo")).to satisfy {|v| (0..1).include?(v) } 673 | end 674 | 675 | it "should namespace expiretime" do 676 | @namespaced.set('mykey', 'Hello') 677 | @namespaced.expireat('mykey', 2000000000) 678 | expect(@namespaced.expiretime('mykey')).to eq(2000000000) 679 | end 680 | 681 | it "should namespace hincrbyfloat" do 682 | @namespaced.hset('mykey', 'field', 10.50) 683 | expect(@namespaced.hincrbyfloat('mykey', 'field', 0.1)).to eq(10.6) 684 | end 685 | 686 | it "should namespace incrbyfloat" do 687 | @namespaced.set('mykey', 10.50) 688 | expect(@namespaced.incrbyfloat('mykey', 0.1)).to eq(10.6) 689 | end 690 | 691 | it "should namespace object" do 692 | @namespaced.set('foo', 1000) 693 | expect(@namespaced.object('encoding', 'foo')).to eq('int') 694 | end 695 | 696 | it "should namespace persist" do 697 | @namespaced.set('mykey', 'Hello') 698 | @namespaced.expire('mykey', 60) 699 | expect(@namespaced.persist('mykey')).to eq(true) 700 | expect(@namespaced.ttl('mykey')).to eq(-1) 701 | end 702 | 703 | it "should namespace pexpire" do 704 | @namespaced.set('mykey', 'Hello') 705 | expect(@namespaced.pexpire('mykey', 60000)).to eq(true) 706 | end 707 | 708 | it "should namespace pexpireat" do 709 | @namespaced.set('mykey', 'Hello') 710 | expect(@namespaced.pexpire('mykey', 1555555555005)).to eq(true) 711 | end 712 | 713 | it "should namespace pexpiretime" do 714 | @namespaced.set('mykey', 'Hello') 715 | @namespaced.pexpireat('mykey', 2000000000000) 716 | expect(@namespaced.pexpiretime('mykey')).to eq(2000000000000) 717 | end 718 | 719 | it "should namespace psetex" do 720 | expect(@namespaced.psetex('mykey', 10000, 'Hello')).to eq('OK') 721 | expect(@namespaced.get('mykey')).to eq('Hello') 722 | end 723 | 724 | it "should namespace pttl" do 725 | @namespaced.set('mykey', 'Hello') 726 | @namespaced.expire('mykey', 1) 727 | expect(@namespaced.pttl('mykey')).to be >= 0 728 | end 729 | 730 | it "should namespace eval keys passed in as array args" do 731 | expect(@namespaced. 732 | eval("return {KEYS[1], KEYS[2]}", %w[k1 k2], %w[arg1 arg2])). 733 | to eq(%w[ns:k1 ns:k2]) 734 | end 735 | 736 | it "should namespace eval keys passed in as hash args" do 737 | expect(@namespaced. 738 | eval("return {KEYS[1], KEYS[2]}", :keys => %w[k1 k2], :argv => %w[arg1 arg2])). 739 | to eq(%w[ns:k1 ns:k2]) 740 | end 741 | 742 | it "should namespace eval keys passed in as hash args unmodified" do 743 | args = { :keys => %w[k1 k2], :argv => %w[arg1 arg2] } 744 | args.freeze 745 | expect(@namespaced. 746 | eval("return {KEYS[1], KEYS[2]}", args)). 747 | to eq(%w[ns:k1 ns:k2]) 748 | end 749 | 750 | context '#evalsha' do 751 | let!(:sha) do 752 | @redis.script(:load, "return {KEYS[1], KEYS[2]}") 753 | end 754 | 755 | it "should namespace evalsha keys passed in as array args" do 756 | expect(@namespaced. 757 | evalsha(sha, %w[k1 k2], %w[arg1 arg2])). 758 | to eq(%w[ns:k1 ns:k2]) 759 | end 760 | 761 | it "should namespace evalsha keys passed in as hash args" do 762 | expect(@namespaced. 763 | evalsha(sha, :keys => %w[k1 k2], :argv => %w[arg1 arg2])). 764 | to eq(%w[ns:k1 ns:k2]) 765 | end 766 | 767 | it "should namespace evalsha keys passed in as hash args unmodified" do 768 | args = { :keys => %w[k1 k2], :argv => %w[arg1 arg2] } 769 | args.freeze 770 | expect(@namespaced. 771 | evalsha(sha, args)). 772 | to eq(%w[ns:k1 ns:k2]) 773 | end 774 | end 775 | 776 | context "in a nested namespace" do 777 | let(:nested_namespace) { Redis::Namespace.new(:nest, :redis => @namespaced) } 778 | let(:sha) { @redis.script(:load, "return {KEYS[1], KEYS[2]}") } 779 | 780 | it "should namespace eval keys passed in as hash args" do 781 | expect(nested_namespace. 782 | eval("return {KEYS[1], KEYS[2]}", :keys => %w[k1 k2], :argv => %w[arg1 arg2])). 783 | to eq(%w[ns:nest:k1 ns:nest:k2]) 784 | end 785 | it "should namespace evalsha keys passed in as hash args" do 786 | expect(nested_namespace.evalsha(sha, :keys => %w[k1 k2], :argv => %w[arg1 arg2])). 787 | to eq(%w[ns:nest:k1 ns:nest:k2]) 788 | end 789 | end 790 | end 791 | end 792 | 793 | # Redis 2.8 RC reports its version as 2.7. 794 | if @redis_version >= Gem::Version.new("2.7.105") 795 | describe "redis 2.8 commands" do 796 | context 'keyspace scan methods' do 797 | let(:keys) do 798 | %w(alpha ns:beta gamma ns:delta ns:epsilon ns:zeta:one ns:zeta:two ns:theta) 799 | end 800 | let(:namespaced_keys) do 801 | keys.map{|k| k.dup.sub!(/\Ans:/,'') }.compact.sort 802 | end 803 | before(:each) do 804 | keys.each do |key| 805 | @redis.set(key, key) 806 | end 807 | end 808 | let(:matching_namespaced_keys) do 809 | namespaced_keys.select{|k| k[/\Azeta:/] }.compact.sort 810 | end 811 | 812 | context '#scan' do 813 | context 'when :match supplied' do 814 | it 'should retrieve the proper keys' do 815 | _, result = @namespaced.scan(0, :match => 'zeta:*', :count => 1000) 816 | expect(result).to match_array(matching_namespaced_keys) 817 | end 818 | end 819 | context 'without :match supplied' do 820 | it 'should retrieve the proper keys' do 821 | _, result = @namespaced.scan(0, :count => 1000) 822 | expect(result).to match_array(namespaced_keys) 823 | end 824 | end 825 | end if Redis.new.respond_to?(:scan) 826 | 827 | context '#scan_each' do 828 | context 'when :match supplied' do 829 | context 'when given a block' do 830 | it 'should yield unnamespaced' do 831 | results = [] 832 | @namespaced.scan_each(:match => 'zeta:*', :count => 1000) {|k| results << k } 833 | expect(results).to match_array(matching_namespaced_keys) 834 | end 835 | end 836 | context 'without a block' do 837 | it 'should return an Enumerator that un-namespaces' do 838 | enum = @namespaced.scan_each(:match => 'zeta:*', :count => 1000) 839 | expect(enum.to_a).to match_array(matching_namespaced_keys) 840 | end 841 | end 842 | end 843 | context 'without :match supplied' do 844 | context 'when given a block' do 845 | it 'should yield unnamespaced' do 846 | results = [] 847 | @namespaced.scan_each(:count => 1000){ |k| results << k } 848 | expect(results).to match_array(namespaced_keys) 849 | end 850 | end 851 | context 'without a block' do 852 | it 'should return an Enumerator that un-namespaces' do 853 | enum = @namespaced.scan_each(:count => 1000) 854 | expect(enum.to_a).to match_array(namespaced_keys) 855 | end 856 | end 857 | end 858 | end if Redis.new.respond_to?(:scan_each) 859 | end 860 | 861 | context 'hash scan methods' do 862 | before(:each) do 863 | @redis.mapped_hmset('hsh', {'zeta:wrong:one' => 'WRONG', 'wrong:two' => 'WRONG'}) 864 | @redis.mapped_hmset('ns:hsh', hash) 865 | end 866 | let(:hash) do 867 | {'zeta:one' => 'OK', 'zeta:two' => 'OK', 'three' => 'OKAY'} 868 | end 869 | let(:hash_matching_subset) do 870 | # select is not consistent from 1.8.7 -> 1.9.2 :( 871 | hash.reject {|k,v| !k[/\Azeta:/] } 872 | end 873 | context '#hscan' do 874 | context 'when supplied :match' do 875 | it 'should retrieve the proper keys' do 876 | _, results = @namespaced.hscan('hsh', 0, :match => 'zeta:*') 877 | expect(results).to match_array(hash_matching_subset.to_a) 878 | end 879 | end 880 | context 'without :match supplied' do 881 | it 'should retrieve all hash keys' do 882 | _, results = @namespaced.hscan('hsh', 0) 883 | expect(results).to match_array(@redis.hgetall('ns:hsh').to_a) 884 | end 885 | end 886 | end if Redis.new.respond_to?(:hscan) 887 | 888 | context '#hscan_each' do 889 | context 'when :match supplied' do 890 | context 'when given a block' do 891 | it 'should yield the correct hash keys unchanged' do 892 | results = [] 893 | @namespaced.hscan_each('hsh', :match => 'zeta:*', :count => 1000) { |kv| results << kv} 894 | expect(results).to match_array(hash_matching_subset.to_a) 895 | end 896 | end 897 | context 'without a block' do 898 | it 'should return an Enumerator that yields the correct hash keys unchanged' do 899 | enum = @namespaced.hscan_each('hsh', :match => 'zeta:*', :count => 1000) 900 | expect(enum.to_a).to match_array(hash_matching_subset.to_a) 901 | end 902 | end 903 | end 904 | context 'without :match supplied' do 905 | context 'when given a block' do 906 | it 'should yield all hash keys unchanged' do 907 | results = [] 908 | @namespaced.hscan_each('hsh', :count => 1000){ |k| results << k } 909 | expect(results).to match_array(hash.to_a) 910 | end 911 | end 912 | context 'without a block' do 913 | it 'should return an Enumerator that yields all keys unchanged' do 914 | enum = @namespaced.hscan_each('hsh', :count => 1000) 915 | expect(enum.to_a).to match_array(hash.to_a) 916 | end 917 | end 918 | end 919 | end if Redis.new.respond_to?(:hscan_each) 920 | end 921 | 922 | context 'set scan methods' do 923 | before(:each) do 924 | set.each { |elem| @namespaced.sadd('set', elem) } 925 | @redis.sadd('set', 'WRONG') 926 | end 927 | let(:set) do 928 | %w(zeta:one zeta:two three) 929 | end 930 | let(:matching_subset) do 931 | set.select { |e| e[/\Azeta:/] } 932 | end 933 | 934 | context '#sscan' do 935 | context 'when supplied :match' do 936 | it 'should retrieve the matching set members from the proper set' do 937 | _, results = @namespaced.sscan('set', 0, :match => 'zeta:*', :count => 1000) 938 | expect(results).to match_array(matching_subset) 939 | end 940 | end 941 | context 'without :match supplied' do 942 | it 'should retrieve all set members from the proper set' do 943 | _, results = @namespaced.sscan('set', 0, :count => 1000) 944 | expect(results).to match_array(set) 945 | end 946 | end 947 | end if Redis.new.respond_to?(:sscan) 948 | 949 | context '#sscan_each' do 950 | context 'when :match supplied' do 951 | context 'when given a block' do 952 | it 'should yield the correct hset elements unchanged' do 953 | results = [] 954 | @namespaced.sscan_each('set', :match => 'zeta:*', :count => 1000) { |kv| results << kv} 955 | expect(results).to match_array(matching_subset) 956 | end 957 | end 958 | context 'without a block' do 959 | it 'should return an Enumerator that yields the correct set elements unchanged' do 960 | enum = @namespaced.sscan_each('set', :match => 'zeta:*', :count => 1000) 961 | expect(enum.to_a).to match_array(matching_subset) 962 | end 963 | end 964 | end 965 | context 'without :match supplied' do 966 | context 'when given a block' do 967 | it 'should yield all set elements unchanged' do 968 | results = [] 969 | @namespaced.sscan_each('set', :count => 1000){ |k| results << k } 970 | expect(results).to match_array(set) 971 | end 972 | end 973 | context 'without a block' do 974 | it 'should return an Enumerator that yields all set elements unchanged' do 975 | enum = @namespaced.sscan_each('set', :count => 1000) 976 | expect(enum.to_a).to match_array(set) 977 | end 978 | end 979 | end 980 | end if Redis.new.respond_to?(:sscan_each) 981 | end 982 | 983 | context 'zset scan methods' do 984 | before(:each) do 985 | hash.each {|member, score| @namespaced.zadd('zset', score, member)} 986 | @redis.zadd('zset', 123.45, 'WRONG') 987 | end 988 | let(:hash) do 989 | {'zeta:one' => 1, 'zeta:two' => 2, 'three' => 3} 990 | end 991 | let(:hash_matching_subset) do 992 | # select is not consistent from 1.8.7 -> 1.9.2 :( 993 | hash.reject {|k,v| !k[/\Azeta:/] } 994 | end 995 | context '#zscan' do 996 | context 'when supplied :match' do 997 | it 'should retrieve the matching set elements and their scores' do 998 | results = [] 999 | @namespaced.zscan_each('zset', :match => 'zeta:*', :count => 1000) { |ms| results << ms } 1000 | expect(results).to match_array(hash_matching_subset.to_a) 1001 | end 1002 | end 1003 | context 'without :match supplied' do 1004 | it 'should retrieve all set elements and their scores' do 1005 | results = [] 1006 | @namespaced.zscan_each('zset', :count => 1000) { |ms| results << ms } 1007 | expect(results).to match_array(hash.to_a) 1008 | end 1009 | end 1010 | end if Redis.new.respond_to?(:zscan) 1011 | 1012 | context '#zscan_each' do 1013 | context 'when :match supplied' do 1014 | context 'when given a block' do 1015 | it 'should yield the correct set elements and scores unchanged' do 1016 | results = [] 1017 | @namespaced.zscan_each('zset', :match => 'zeta:*', :count => 1000) { |ms| results << ms} 1018 | expect(results).to match_array(hash_matching_subset.to_a) 1019 | end 1020 | end 1021 | context 'without a block' do 1022 | it 'should return an Enumerator that yields the correct set elements and scoresunchanged' do 1023 | enum = @namespaced.zscan_each('zset', :match => 'zeta:*', :count => 1000) 1024 | expect(enum.to_a).to match_array(hash_matching_subset.to_a) 1025 | end 1026 | end 1027 | end 1028 | context 'without :match supplied' do 1029 | context 'when given a block' do 1030 | it 'should yield all set elements and scores unchanged' do 1031 | results = [] 1032 | @namespaced.zscan_each('zset', :count => 1000){ |ms| results << ms } 1033 | expect(results).to match_array(hash.to_a) 1034 | end 1035 | end 1036 | context 'without a block' do 1037 | it 'should return an Enumerator that yields all set elements and scores unchanged' do 1038 | enum = @namespaced.zscan_each('zset', :count => 1000) 1039 | expect(enum.to_a).to match_array(hash.to_a) 1040 | end 1041 | end 1042 | end 1043 | end if Redis.new.respond_to?(:zscan_each) 1044 | end 1045 | end 1046 | end 1047 | 1048 | if @redis_version >= Gem::Version.new("2.8.9") 1049 | it 'should namespace pfadd' do 1050 | 5.times { |n| @namespaced.pfadd("pf", n) } 1051 | expect(@redis.pfcount("ns:pf")).to eq(5) 1052 | end 1053 | 1054 | it 'should namespace pfcount' do 1055 | 5.times { |n| @redis.pfadd("ns:pf", n) } 1056 | expect(@namespaced.pfcount("pf")).to eq(5) 1057 | end 1058 | 1059 | it 'should namespace pfmerge' do 1060 | 5.times do |n| 1061 | @redis.pfadd("ns:pfa", n) 1062 | @redis.pfadd("ns:pfb", n+5) 1063 | end 1064 | 1065 | @namespaced.pfmerge("pfc", "pfa", "pfb") 1066 | expect(@redis.pfcount("ns:pfc")).to eq(10) 1067 | end 1068 | end 1069 | 1070 | if @redis_version >= Gem::Version.new("3.2.0") 1071 | it 'should namespace bitfield' do 1072 | @namespaced.bitfield("bf", "SET", "i8", 0, "A".ord) 1073 | expect(@redis.get("ns:bf")).to eq("A") 1074 | end 1075 | end 1076 | 1077 | describe :full_namespace do 1078 | it "should return the full namespace including sub namespaces" do 1079 | sub_namespaced = Redis::Namespace.new(:sub1, :redis => @namespaced) 1080 | sub_sub_namespaced = Redis::Namespace.new(:sub2, :redis => sub_namespaced) 1081 | 1082 | expect(@namespaced.full_namespace).to eql("ns") 1083 | expect(sub_namespaced.full_namespace).to eql("ns:sub1") 1084 | expect(sub_sub_namespaced.full_namespace).to eql("ns:sub1:sub2") 1085 | end 1086 | end 1087 | 1088 | describe :clear do 1089 | it "warns with helpful output" do 1090 | expect { @namespaced.clear }.to output(/can run for a very long time/).to_stderr 1091 | end 1092 | 1093 | it "should delete all the keys" do 1094 | @redis.set("foo", "bar") 1095 | @namespaced.mset("foo1", "bar", "foo2", "bar") 1096 | capture_stderr { @namespaced.clear } 1097 | 1098 | expect(@redis.keys).to eq ["foo"] 1099 | expect(@namespaced.keys).to be_empty 1100 | end 1101 | 1102 | it "should delete all the keys in older redis" do 1103 | allow(@redis).to receive(:info).and_return({ "redis_version" => "2.7.0" }) 1104 | 1105 | @redis.set("foo", "bar") 1106 | @namespaced.mset("foo1", "bar", "foo2", "bar") 1107 | capture_stderr { @namespaced.clear } 1108 | 1109 | expect(@redis.keys).to eq ["foo"] 1110 | expect(@namespaced.keys).to be_empty 1111 | end 1112 | end 1113 | end 1114 | --------------------------------------------------------------------------------