ohm 1.4.0 → 2.0.0.alpha1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gems +4 -4
- data/CHANGELOG +0 -8
- data/README.md +4 -6
- data/examples/chaining.rb +52 -56
- data/lib/ohm.rb +145 -358
- data/lib/ohm/command.rb +14 -10
- data/lib/ohm/lua/delete.lua +51 -0
- data/lib/ohm/lua/save.lua +104 -0
- data/ohm.gemspec +8 -6
- data/test/command.rb +22 -24
- data/test/connection.rb +5 -89
- data/test/filtering.rb +140 -8
- data/test/helper.rb +3 -1
- data/test/indices.rb +1 -1
- data/test/issue-52.rb +2 -2
- data/test/json.rb +0 -16
- data/test/list.rb +1 -1
- data/test/model.rb +70 -194
- data/test/uniques.rb +2 -32
- metadata +23 -26
- data/lib/ohm/transaction.rb +0 -133
- data/test/1.8.6_test.rb +0 -25
- data/test/pipeline-performance.rb +0 -67
- data/test/transactions.rb +0 -240
- data/test/validations.rb +0 -195
data/lib/ohm/command.rb
CHANGED
@@ -16,14 +16,17 @@ module Ohm
|
|
16
16
|
@keys = []
|
17
17
|
end
|
18
18
|
|
19
|
-
def call(
|
20
|
-
newkey(
|
21
|
-
|
19
|
+
def call(nido, redis)
|
20
|
+
newkey(nido, redis) do |key|
|
21
|
+
redis.call(@operation, key, *params(nido, redis))
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
25
|
def clean
|
26
|
-
keys.each
|
26
|
+
keys.each do |key, redis|
|
27
|
+
redis.call("DEL", key)
|
28
|
+
end
|
29
|
+
|
27
30
|
subcommands.each { |cmd| cmd.clean }
|
28
31
|
end
|
29
32
|
|
@@ -32,15 +35,16 @@ module Ohm
|
|
32
35
|
args.select { |arg| arg.respond_to?(:call) }
|
33
36
|
end
|
34
37
|
|
35
|
-
def params(
|
36
|
-
args.map { |arg| arg.respond_to?(:call) ? arg.call(
|
38
|
+
def params(nido, redis)
|
39
|
+
args.map { |arg| arg.respond_to?(:call) ? arg.call(nido, redis) : arg }
|
37
40
|
end
|
38
41
|
|
39
|
-
def newkey(
|
40
|
-
key =
|
41
|
-
keys << key
|
42
|
+
def newkey(nido, redis)
|
43
|
+
key = nido[SecureRandom.hex(32)]
|
44
|
+
keys << [key, redis]
|
45
|
+
|
46
|
+
yield key
|
42
47
|
|
43
|
-
yield key
|
44
48
|
return key
|
45
49
|
end
|
46
50
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
local model = cmsgpack.unpack(ARGV[1])
|
2
|
+
local uniques = cmsgpack.unpack(ARGV[2])
|
3
|
+
local collections = cmsgpack.unpack(ARGV[3])
|
4
|
+
|
5
|
+
local function remove_indices(model)
|
6
|
+
local memo = model.key .. ":_indices"
|
7
|
+
local existing = redis.call("SMEMBERS", memo)
|
8
|
+
|
9
|
+
for _, key in ipairs(existing) do
|
10
|
+
redis.call("SREM", key, model.id)
|
11
|
+
redis.call("SREM", memo, key)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
local function remove_uniques(model, uniques)
|
16
|
+
local memo = model.key .. ":_uniques"
|
17
|
+
|
18
|
+
for field, _ in pairs(uniques) do
|
19
|
+
local key = model.name .. ":uniques:" .. field
|
20
|
+
|
21
|
+
redis.call("HDEL", key, redis.call("HGET", memo, key))
|
22
|
+
redis.call("HDEL", memo, key)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
local function remove_collections(model, collections)
|
27
|
+
for _, collection in ipairs(collections) do
|
28
|
+
local key = model.key .. ":" .. collection
|
29
|
+
|
30
|
+
redis.call("DEL", key)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
local function delete(model)
|
35
|
+
local keys = {
|
36
|
+
model.key .. ":counters",
|
37
|
+
model.key .. ":_indices",
|
38
|
+
model.key .. ":_uniques",
|
39
|
+
model.key
|
40
|
+
}
|
41
|
+
|
42
|
+
redis.call("SREM", model.name .. ":all", model.id)
|
43
|
+
redis.call("DEL", unpack(keys))
|
44
|
+
end
|
45
|
+
|
46
|
+
remove_indices(model)
|
47
|
+
remove_uniques(model, uniques)
|
48
|
+
remove_collections(model, collections)
|
49
|
+
delete(model)
|
50
|
+
|
51
|
+
return model.id
|
@@ -0,0 +1,104 @@
|
|
1
|
+
local model = cmsgpack.unpack(ARGV[1])
|
2
|
+
local attrs = cmsgpack.unpack(ARGV[2])
|
3
|
+
local indices = cmsgpack.unpack(ARGV[3])
|
4
|
+
local uniques = cmsgpack.unpack(ARGV[4])
|
5
|
+
|
6
|
+
local function save(model, attrs)
|
7
|
+
redis.call("SADD", model.name .. ":all", model.id)
|
8
|
+
redis.call("DEL", model.key)
|
9
|
+
|
10
|
+
if math.mod(#attrs, 2) == 1 then
|
11
|
+
error("Wrong number of attribute/value pairs")
|
12
|
+
end
|
13
|
+
|
14
|
+
if #attrs > 0 then
|
15
|
+
redis.call("HMSET", model.key, unpack(attrs))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
local function index(model, indices)
|
20
|
+
for field, enum in pairs(indices) do
|
21
|
+
for _, val in ipairs(enum) do
|
22
|
+
local key = model.name .. ":indices:" .. field .. ":" .. tostring(val)
|
23
|
+
|
24
|
+
redis.call("SADD", model.key .. ":_indices", key)
|
25
|
+
redis.call("SADD", key, model.id)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
local function remove_indices(model)
|
31
|
+
local memo = model.key .. ":_indices"
|
32
|
+
local existing = redis.call("SMEMBERS", memo)
|
33
|
+
|
34
|
+
for _, key in ipairs(existing) do
|
35
|
+
redis.call("SREM", key, model.id)
|
36
|
+
redis.call("SREM", memo, key)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
local function unique(model, uniques)
|
41
|
+
for field, value in pairs(uniques) do
|
42
|
+
local key = model.name .. ":uniques:" .. field
|
43
|
+
|
44
|
+
redis.call("HSET", model.key .. ":_uniques", key, value)
|
45
|
+
redis.call("HSET", key, value, model.id)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
local function remove_uniques(model, uniques)
|
50
|
+
local memo = model.key .. ":_uniques"
|
51
|
+
|
52
|
+
for field, _ in pairs(uniques) do
|
53
|
+
local key = model.name .. ":uniques:" .. field
|
54
|
+
|
55
|
+
redis.call("HDEL", key, redis.call("HGET", memo, key))
|
56
|
+
redis.call("HDEL", memo, key)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
local function verify(model, uniques)
|
61
|
+
local duplicates = {}
|
62
|
+
|
63
|
+
for field, value in pairs(uniques) do
|
64
|
+
local key = model.name .. ":uniques:" .. field
|
65
|
+
local id = redis.call("HGET", key, tostring(value))
|
66
|
+
|
67
|
+
if id and id ~= tostring(model.id) then
|
68
|
+
duplicates[#duplicates + 1] = field
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
return duplicates, #duplicates ~= 0
|
73
|
+
end
|
74
|
+
|
75
|
+
local duplicates, err = verify(model, uniques)
|
76
|
+
|
77
|
+
if err then
|
78
|
+
error("UniqueIndexViolation: " .. duplicates[1])
|
79
|
+
end
|
80
|
+
|
81
|
+
local function convertBooleans(list)
|
82
|
+
for index, value in ipairs(list) do
|
83
|
+
if type(value) == "boolean" then
|
84
|
+
if value then
|
85
|
+
list[index] = 1
|
86
|
+
else
|
87
|
+
list[index] = nil
|
88
|
+
list[index - 1] = nil
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
convertBooleans(attrs)
|
95
|
+
|
96
|
+
save(model, attrs)
|
97
|
+
|
98
|
+
remove_indices(model)
|
99
|
+
index(model, indices)
|
100
|
+
|
101
|
+
remove_uniques(model, uniques)
|
102
|
+
unique(model, uniques)
|
103
|
+
|
104
|
+
return model.id
|
data/ohm.gemspec
CHANGED
@@ -1,18 +1,20 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "ohm"
|
3
|
-
s.version = "
|
3
|
+
s.version = "2.0.0.alpha1"
|
4
4
|
s.summary = %{Object-hash mapping library for Redis.}
|
5
5
|
s.description = %Q{Ohm is a library that allows to store an object in Redis, a persistent key-value database. It includes an extensible list of validations and has very good performance.}
|
6
6
|
s.authors = ["Michel Martens", "Damian Janowski", "Cyril David"]
|
7
7
|
s.email = ["michel@soveran.com", "djanowski@dimaion.com", "me@cyrildavid.com"]
|
8
|
-
s.homepage = "http://soveran.github.
|
8
|
+
s.homepage = "http://soveran.github.io/ohm/"
|
9
9
|
s.license = "MIT"
|
10
10
|
|
11
11
|
s.files = `git ls-files`.split("\n")
|
12
12
|
|
13
13
|
s.rubyforge_project = "ohm"
|
14
|
-
|
15
|
-
s.add_dependency "
|
16
|
-
s.add_dependency "
|
17
|
-
s.
|
14
|
+
|
15
|
+
s.add_dependency "redic"
|
16
|
+
s.add_dependency "nido"
|
17
|
+
s.add_dependency "msgpack"
|
18
|
+
|
19
|
+
s.add_development_dependency "cutest"
|
18
20
|
end
|
data/test/command.rb
CHANGED
@@ -2,56 +2,54 @@ require_relative "helper"
|
|
2
2
|
|
3
3
|
scope do
|
4
4
|
setup do
|
5
|
-
redis =
|
6
|
-
redis.
|
5
|
+
redis = Redic.new
|
6
|
+
redis.call("FLUSHDB")
|
7
7
|
|
8
|
-
|
9
|
-
# redis.client.logger = Logger.new(STDOUT)
|
10
|
-
nest = Nest.new("User:tmp", redis)
|
8
|
+
nido = Nido.new("User:tmp")
|
11
9
|
|
12
|
-
[1, 2, 3].each { |i| redis.
|
13
|
-
[1, 4, 5].each { |i| redis.
|
10
|
+
[1, 2, 3].each { |i| redis.call("SADD", "A", i) }
|
11
|
+
[1, 4, 5].each { |i| redis.call("SADD", "B", i) }
|
14
12
|
|
15
|
-
[10, 11, 12].each { |i| redis.
|
16
|
-
[11, 12, 13].each { |i| redis.
|
17
|
-
[12, 13, 14].each { |i| redis.
|
13
|
+
[10, 11, 12].each { |i| redis.call("SADD", "C", i) }
|
14
|
+
[11, 12, 13].each { |i| redis.call("SADD", "D", i) }
|
15
|
+
[12, 13, 14].each { |i| redis.call("SADD", "E", i) }
|
18
16
|
|
19
|
-
[10, 11, 12].each { |i| redis.
|
20
|
-
[11, 12, 13].each { |i| redis.
|
21
|
-
[12, 13, 14].each { |i| redis.
|
17
|
+
[10, 11, 12].each { |i| redis.call("SADD", "F", i) }
|
18
|
+
[11, 12, 13].each { |i| redis.call("SADD", "G", i) }
|
19
|
+
[12, 13, 14].each { |i| redis.call("SADD", "H", i) }
|
22
20
|
|
23
|
-
[redis,
|
21
|
+
[redis, nido]
|
24
22
|
end
|
25
23
|
|
26
24
|
test "special condition: single argument returns that arg" do
|
27
25
|
assert_equal "A", Ohm::Command[:sinterstore, "A"]
|
28
26
|
end
|
29
27
|
|
30
|
-
test "full stack test" do |redis,
|
28
|
+
test "full stack test" do |redis, nido|
|
31
29
|
cmd1 = Ohm::Command[:sinterstore, "A", "B"]
|
32
30
|
|
33
|
-
res = cmd1.call(
|
34
|
-
assert_equal ["1"], res
|
31
|
+
res = cmd1.call(nido, redis)
|
32
|
+
assert_equal ["1"], redis.call("SMEMBERS", res)
|
35
33
|
|
36
34
|
cmd1.clean
|
37
|
-
|
35
|
+
assert_equal 0, redis.call("EXISTS", res)
|
38
36
|
|
39
37
|
cmd2 = Ohm::Command[:sinterstore, "C", "D", "E"]
|
40
38
|
cmd3 = Ohm::Command[:sunionstore, cmd1, cmd2]
|
41
39
|
|
42
|
-
res = cmd3.call(
|
43
|
-
assert_equal ["1", "12"], res
|
40
|
+
res = cmd3.call(nido, redis)
|
41
|
+
assert_equal ["1", "12"], redis.call("SMEMBERS", res)
|
44
42
|
|
45
43
|
cmd3.clean
|
46
|
-
assert redis.
|
44
|
+
assert redis.call("KEYS", nido["*"]).empty?
|
47
45
|
|
48
46
|
cmd4 = Ohm::Command[:sinterstore, "F", "G", "H"]
|
49
47
|
cmd5 = Ohm::Command[:sdiffstore, cmd3, cmd4]
|
50
48
|
|
51
|
-
res = cmd5.call(
|
52
|
-
assert_equal ["1"], res
|
49
|
+
res = cmd5.call(nido, redis)
|
50
|
+
assert_equal ["1"], redis.call("SMEMBERS", res)
|
53
51
|
|
54
52
|
cmd5.clean
|
55
|
-
assert redis.
|
53
|
+
assert redis.call("KEYS", nido["*"]).empty?
|
56
54
|
end
|
57
55
|
end
|
data/test/connection.rb
CHANGED
@@ -2,105 +2,21 @@
|
|
2
2
|
|
3
3
|
require File.expand_path("./helper", File.dirname(__FILE__))
|
4
4
|
|
5
|
-
unless defined?(
|
6
|
-
|
7
|
-
end
|
8
|
-
|
9
|
-
prepare.clear
|
10
|
-
|
11
|
-
test "no rewriting of settings hash when using Ohm.connect" do
|
12
|
-
settings = { :url => "redis://127.0.0.1:6379/15" }.freeze
|
13
|
-
|
14
|
-
ex = nil
|
15
|
-
|
16
|
-
begin
|
17
|
-
Ohm.connect(settings)
|
18
|
-
rescue RuntimeError => e
|
19
|
-
ex = e
|
20
|
-
end
|
21
|
-
|
22
|
-
assert_equal ex, nil
|
23
|
-
end
|
24
|
-
|
25
|
-
test "connects lazily" do
|
26
|
-
Ohm.connect(:port => 9876)
|
27
|
-
|
28
|
-
begin
|
29
|
-
Ohm.redis.get "foo"
|
30
|
-
rescue => e
|
31
|
-
assert_equal Redis::CannotConnectError, e.class
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
test "provides a separate connection for each thread" do
|
36
|
-
assert Ohm.redis == Ohm.redis
|
37
|
-
|
38
|
-
conn1, conn2 = nil
|
39
|
-
|
40
|
-
threads = []
|
41
|
-
|
42
|
-
threads << Thread.new do
|
43
|
-
conn1 = Ohm.redis
|
44
|
-
end
|
45
|
-
|
46
|
-
threads << Thread.new do
|
47
|
-
conn2 = Ohm.redis
|
48
|
-
end
|
49
|
-
|
50
|
-
threads.each { |t| t.join }
|
51
|
-
|
52
|
-
assert conn1 != conn2
|
53
|
-
end
|
54
|
-
|
55
|
-
test "supports connecting by URL" do
|
56
|
-
Ohm.connect(:url => "redis://localhost:9876")
|
57
|
-
|
58
|
-
begin
|
59
|
-
Ohm.redis.get "foo"
|
60
|
-
rescue => e
|
61
|
-
assert_equal Redis::CannotConnectError, e.class
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
setup do
|
66
|
-
Ohm.connect(:url => "redis://localhost:6379/0")
|
67
|
-
end
|
68
|
-
|
69
|
-
test "connection class" do
|
70
|
-
conn = Ohm::Connection.new(:foo, :url => "redis://localhost:6379/0")
|
71
|
-
|
72
|
-
assert conn.redis.kind_of?(Redis)
|
73
|
-
end
|
74
|
-
|
75
|
-
test "issue #46" do
|
76
|
-
class B < Ohm::Model
|
77
|
-
connect(:url => "redis://localhost:6379/15")
|
78
|
-
end
|
79
|
-
|
80
|
-
# We do this since we did prepare.clear above.
|
81
|
-
B.db.flushall
|
82
|
-
|
83
|
-
b1, b2 = nil, nil
|
84
|
-
|
85
|
-
Thread.new { b1 = B.create }.join
|
86
|
-
Thread.new { b2 = B.create }.join
|
87
|
-
|
88
|
-
assert_equal [b1, b2], B.all.sort.to_a
|
5
|
+
unless defined?(Redic::CannotConnectError)
|
6
|
+
Redic::CannotConnectError = Errno::ECONNREFUSED
|
89
7
|
end
|
90
8
|
|
91
9
|
test "model can define its own connection" do
|
92
10
|
class B < Ohm::Model
|
93
|
-
|
11
|
+
self.redis = Redic.new("redis://localhost:6379/1")
|
94
12
|
end
|
95
13
|
|
96
|
-
|
97
|
-
assert_equal Ohm.conn.options, {:url=>"redis://localhost:6379/0"}
|
14
|
+
assert B.redis.url != Ohm.redis.url
|
98
15
|
end
|
99
16
|
|
100
17
|
test "model inherits Ohm.redis connection by default" do
|
101
|
-
Ohm.connect(:url => "redis://localhost:9876")
|
102
18
|
class C < Ohm::Model
|
103
19
|
end
|
104
20
|
|
105
|
-
assert_equal C.
|
21
|
+
assert_equal C.redis.url, Ohm.redis.url
|
106
22
|
end
|
data/test/filtering.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
__END__
|
1
2
|
require File.expand_path("./helper", File.dirname(__FILE__))
|
2
3
|
|
3
4
|
class User < Ohm::Model
|
@@ -95,14 +96,6 @@ test "#union" do |john, jane|
|
|
95
96
|
assert res.any? { |e| e.status == "inactive" }
|
96
97
|
end
|
97
98
|
|
98
|
-
test "#combine" do |john, jane|
|
99
|
-
res = User.find(:status => "active").combine(fname: ["John", "Jane"])
|
100
|
-
|
101
|
-
assert_equal 2, res.size
|
102
|
-
assert res.include?(john)
|
103
|
-
assert res.include?(jane)
|
104
|
-
end
|
105
|
-
|
106
99
|
# book author thing via @myobie
|
107
100
|
scope do
|
108
101
|
class Book < Ohm::Model
|
@@ -167,3 +160,142 @@ scope do
|
|
167
160
|
assert_equal 2, res.size
|
168
161
|
end
|
169
162
|
end
|
163
|
+
|
164
|
+
# test precision of filtering commands
|
165
|
+
require "logger"
|
166
|
+
require "stringio"
|
167
|
+
scope do
|
168
|
+
class Post < Ohm::Model
|
169
|
+
attribute :author
|
170
|
+
index :author
|
171
|
+
|
172
|
+
attribute :mood
|
173
|
+
index :mood
|
174
|
+
end
|
175
|
+
|
176
|
+
setup do
|
177
|
+
io = StringIO.new
|
178
|
+
|
179
|
+
Post.connect(:logger => Logger.new(io))
|
180
|
+
|
181
|
+
Post.create(author: "matz", mood: "happy")
|
182
|
+
Post.create(author: "rich", mood: "mad")
|
183
|
+
|
184
|
+
io
|
185
|
+
end
|
186
|
+
|
187
|
+
def read(io)
|
188
|
+
io.rewind
|
189
|
+
io.read
|
190
|
+
end
|
191
|
+
|
192
|
+
test "SINTERSTORE a b" do |io|
|
193
|
+
Post.find(author: "matz").find(mood: "happy").to_a
|
194
|
+
|
195
|
+
# This is the simple case. We should only do one SINTERSTORE
|
196
|
+
# given two direct keys. Anything more and we're performing badly.
|
197
|
+
expected = "SINTERSTORE Post:tmp:[a-f0-9]{64} " +
|
198
|
+
"Post:indices:author:matz Post:indices:mood:happy"
|
199
|
+
|
200
|
+
assert(read(io) =~ Regexp.new(expected))
|
201
|
+
end
|
202
|
+
|
203
|
+
test "SUNIONSTORE a b" do |io|
|
204
|
+
Post.find(author: "matz").union(mood: "happy").to_a
|
205
|
+
|
206
|
+
# Another simple case where we must only do one operation at maximum.
|
207
|
+
expected = "SUNIONSTORE Post:tmp:[a-f0-9]{64} " +
|
208
|
+
"Post:indices:author:matz Post:indices:mood:happy"
|
209
|
+
|
210
|
+
assert(read(io) =~ Regexp.new(expected))
|
211
|
+
end
|
212
|
+
|
213
|
+
test "SUNIONSTORE c (SINTERSTORE a b)" do |io|
|
214
|
+
Post.find(author: "matz").find(mood: "happy").union(author: "rich").to_a
|
215
|
+
|
216
|
+
# For this case we need an intermediate key. This will
|
217
|
+
# contain the intersection of matz + happy.
|
218
|
+
expected = "SINTERSTORE (Post:tmp:[a-f0-9]{64}) " +
|
219
|
+
"Post:indices:author:matz Post:indices:mood:happy"
|
220
|
+
|
221
|
+
assert(read(io) =~ Regexp.new(expected))
|
222
|
+
|
223
|
+
# The next operation is simply doing a UNION of the previously
|
224
|
+
# generated intermediate key and the additional single key.
|
225
|
+
expected = "SUNIONSTORE (Post:tmp:[a-f0-9]{64}) " +
|
226
|
+
"%s Post:indices:author:rich" % $1
|
227
|
+
|
228
|
+
assert(read(io) =~ Regexp.new(expected))
|
229
|
+
end
|
230
|
+
|
231
|
+
test "SUNIONSTORE (SINTERSTORE c d) (SINTERSTORE a b)" do |io|
|
232
|
+
Post.find(author: "matz").find(mood: "happy").
|
233
|
+
union(author: "rich", mood: "sad").to_a
|
234
|
+
|
235
|
+
# Similar to the previous case, we need to do an intermediate
|
236
|
+
# operation.
|
237
|
+
expected = "SINTERSTORE (Post:tmp:[a-f0-9]{64}) " +
|
238
|
+
"Post:indices:author:matz Post:indices:mood:happy"
|
239
|
+
|
240
|
+
match1 = read(io).match(Regexp.new(expected))
|
241
|
+
assert match1
|
242
|
+
|
243
|
+
# But now, we need to also hold another intermediate key for the
|
244
|
+
# condition of author: rich AND mood: sad.
|
245
|
+
expected = "SINTERSTORE (Post:tmp:[a-f0-9]{64}) " +
|
246
|
+
"Post:indices:author:rich Post:indices:mood:sad"
|
247
|
+
|
248
|
+
match2 = read(io).match(Regexp.new(expected))
|
249
|
+
assert match2
|
250
|
+
|
251
|
+
# Now we expect that it does a UNION of those two previous
|
252
|
+
# intermediate keys.
|
253
|
+
expected = sprintf(
|
254
|
+
"SUNIONSTORE (Post:tmp:[a-f0-9]{64}) %s %s",
|
255
|
+
match1[1], match2[1]
|
256
|
+
)
|
257
|
+
|
258
|
+
assert(read(io) =~ Regexp.new(expected))
|
259
|
+
end
|
260
|
+
|
261
|
+
test do |io|
|
262
|
+
Post.create(author: "kent", mood: "sad")
|
263
|
+
|
264
|
+
Post.find(author: "kent", mood: "sad").
|
265
|
+
union(author: "matz", mood: "happy").
|
266
|
+
except(mood: "sad", author: "rich").to_a
|
267
|
+
|
268
|
+
expected = "SINTERSTORE (Post:tmp:[a-f0-9]{64}) " +
|
269
|
+
"Post:indices:author:kent Post:indices:mood:sad"
|
270
|
+
|
271
|
+
match1 = read(io).match(Regexp.new(expected))
|
272
|
+
assert match1
|
273
|
+
|
274
|
+
expected = "SINTERSTORE (Post:tmp:[a-f0-9]{64}) " +
|
275
|
+
"Post:indices:author:matz Post:indices:mood:happy"
|
276
|
+
|
277
|
+
match2 = read(io).match(Regexp.new(expected))
|
278
|
+
assert match2
|
279
|
+
|
280
|
+
expected = sprintf(
|
281
|
+
"SUNIONSTORE (Post:tmp:[a-f0-9]{64}) %s %s",
|
282
|
+
match1[1], match2[1]
|
283
|
+
)
|
284
|
+
|
285
|
+
match3 = read(io).match(Regexp.new(expected))
|
286
|
+
assert match3
|
287
|
+
|
288
|
+
expected = "SINTERSTORE (Post:tmp:[a-f0-9]{64}) " +
|
289
|
+
"Post:indices:mood:sad Post:indices:author:rich"
|
290
|
+
|
291
|
+
match4 = read(io).match(Regexp.new(expected))
|
292
|
+
assert match4
|
293
|
+
|
294
|
+
expected = sprintf(
|
295
|
+
"SDIFFSTORE (Post:tmp:[a-f0-9]{64}) %s %s",
|
296
|
+
match3[1], match4[1]
|
297
|
+
)
|
298
|
+
|
299
|
+
assert(read(io) =~ Regexp.new(expected))
|
300
|
+
end
|
301
|
+
end
|