ohm 1.4.0 → 2.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/ohm/command.rb CHANGED
@@ -16,14 +16,17 @@ module Ohm
16
16
  @keys = []
17
17
  end
18
18
 
19
- def call(nest, db)
20
- newkey(nest) do |key|
21
- db.send(@operation, key, *params(nest, db))
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 { |key| key.del }
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(nest, db)
36
- args.map { |arg| arg.respond_to?(:call) ? arg.call(nest, db) : arg }
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(nest)
40
- key = nest[SecureRandom.hex(32)]
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 = "1.4.0"
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.com/ohm/"
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
- s.add_dependency "redis"
15
- s.add_dependency "nest", "~> 1.0"
16
- s.add_dependency "scrivener", "~> 0.0.3"
17
- s.add_development_dependency "cutest", "~> 1.1"
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 = Redis.connect
6
- redis.flushdb
5
+ redis = Redic.new
6
+ redis.call("FLUSHDB")
7
7
 
8
- # require 'logger'
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.sadd("A", i) }
13
- [1, 4, 5].each { |i| redis.sadd("B", i) }
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.sadd("C", i) }
16
- [11, 12, 13].each { |i| redis.sadd("D", i) }
17
- [12, 13, 14].each { |i| redis.sadd("E", i) }
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.sadd("F", i) }
20
- [11, 12, 13].each { |i| redis.sadd("G", i) }
21
- [12, 13, 14].each { |i| redis.sadd("H", i) }
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, nest]
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, nest|
28
+ test "full stack test" do |redis, nido|
31
29
  cmd1 = Ohm::Command[:sinterstore, "A", "B"]
32
30
 
33
- res = cmd1.call(nest, redis)
34
- assert_equal ["1"], res.smembers
31
+ res = cmd1.call(nido, redis)
32
+ assert_equal ["1"], redis.call("SMEMBERS", res)
35
33
 
36
34
  cmd1.clean
37
- assert ! res.exists
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(nest, redis)
43
- assert_equal ["1", "12"], res.smembers
40
+ res = cmd3.call(nido, redis)
41
+ assert_equal ["1", "12"], redis.call("SMEMBERS", res)
44
42
 
45
43
  cmd3.clean
46
- assert redis.keys(nest["*"]).empty?
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(nest, redis)
52
- assert_equal ["1"], res.smembers
49
+ res = cmd5.call(nido, redis)
50
+ assert_equal ["1"], redis.call("SMEMBERS", res)
53
51
 
54
52
  cmd5.clean
55
- assert redis.keys(nest["*"]).empty?
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?(Redis::CannotConnectError)
6
- Redis::CannotConnectError = Errno::ECONNREFUSED
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
- connect(:url => "redis://localhost:6379/1")
11
+ self.redis = Redic.new("redis://localhost:6379/1")
94
12
  end
95
13
 
96
- assert_equal B.conn.options, {:url=>"redis://localhost:6379/1"}
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.conn.options, Ohm.conn.options
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