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.
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