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.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8e636620601ef7b7ea32c20094a7e4752f438d95
|
4
|
+
data.tar.gz: 09eabd0899085d76c4d0ddb5fc037459f2b424da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2f5caab8eb261e8381c1c3ea8b9c36da0cc2adb0785bcd70dc2d6d128032d783e0c50e607888fe3f99ab89c097fe0056c11f911637755a10961ac3e19d156994
|
7
|
+
data.tar.gz: 71f3247c75a69a7bc5f4452ab9bf4b70824110f6d4d439d5e1510ad7565bfd64f81ac5dd7d7354dbf136bb983e7919657e93f3b1810a8ed8ad9731941c19ab67
|
data/.gems
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
nido -v 0.0.1
|
2
|
+
cutest -v 1.2.0
|
3
|
+
msgpack -v 0.5.4
|
4
|
+
redic -v 0.0.5
|
data/CHANGELOG
CHANGED
@@ -1,11 +1,3 @@
|
|
1
|
-
1.3.2
|
2
|
-
|
3
|
-
- Fetching a batch of objects is now done in batches of 1000 objects at
|
4
|
-
a time. If you are iterating over large collections, this change should
|
5
|
-
provide a significant performance boost both in used memory and total
|
6
|
-
execution time.
|
7
|
-
- MutableSet#<< is now an alias for #add.
|
8
|
-
|
9
1
|
1.3.1
|
10
2
|
|
11
3
|
- Improve memory consumption when indexing persisted attributes.
|
data/README.md
CHANGED
@@ -443,7 +443,8 @@ lookups.
|
|
443
443
|
In the `Event` example, the index on the name attribute will
|
444
444
|
allow for searches like `Event.find(:name => "some value")`.
|
445
445
|
|
446
|
-
Note that the
|
446
|
+
Note that the {Ohm::Model::Validations#assert_unique assert_unique}
|
447
|
+
validation and the methods {Ohm::Model::Set#find find} and
|
447
448
|
{Ohm::Model::Set#except except} need a corresponding index in order to work.
|
448
449
|
|
449
450
|
### Finding records
|
@@ -461,11 +462,8 @@ User.find(:username => "Albert")
|
|
461
462
|
# Find all users from Argentina
|
462
463
|
User.find(:country => "Argentina")
|
463
464
|
|
464
|
-
# Find all
|
465
|
-
User.find(:country => "Argentina", :status => "
|
466
|
-
|
467
|
-
# Find all active users from Argentina and Uruguay
|
468
|
-
User.find(status: "active").combine(country: ["Argentina", "Uruguay"])
|
465
|
+
# Find all activated users from Argentina
|
466
|
+
User.find(:country => "Argentina", :status => "activated")
|
469
467
|
|
470
468
|
# Find all users from Argentina, except those with a suspended account.
|
471
469
|
User.find(:country => "Argentina").except(:status => "suspended")
|
data/examples/chaining.rb
CHANGED
@@ -21,7 +21,7 @@ require "ohm"
|
|
21
21
|
# end
|
22
22
|
#
|
23
23
|
class User < Ohm::Model
|
24
|
-
collection :orders,
|
24
|
+
collection :orders, Order
|
25
25
|
end
|
26
26
|
|
27
27
|
# The product for our purposes will only contain a name.
|
@@ -41,8 +41,8 @@ class Order < Ohm::Model
|
|
41
41
|
attribute :state
|
42
42
|
index :state
|
43
43
|
|
44
|
-
reference :user,
|
45
|
-
reference :product,
|
44
|
+
reference :user, User
|
45
|
+
reference :product, Product
|
46
46
|
end
|
47
47
|
|
48
48
|
##### Testing what we have so far.
|
@@ -58,26 +58,15 @@ prepare { Ohm.flush }
|
|
58
58
|
setup do
|
59
59
|
@user = User.create
|
60
60
|
|
61
|
-
@ipod = Product.create(name
|
62
|
-
@ipad = Product.create(name
|
61
|
+
@ipod = Product.create(:name => "iPod")
|
62
|
+
@ipad = Product.create(:name => "iPad")
|
63
63
|
|
64
|
-
@pending =
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
@authorized = Order.create(
|
71
|
-
user: @user,
|
72
|
-
state: "authorized",
|
73
|
-
product: @ipad
|
74
|
-
)
|
75
|
-
|
76
|
-
@captured = Order.create(
|
77
|
-
user: @user,
|
78
|
-
state: "captured",
|
79
|
-
product: @ipad
|
80
|
-
)
|
64
|
+
@pending = Order.create(:user => @user, :state => "pending",
|
65
|
+
:product => @ipod)
|
66
|
+
@authorized = Order.create(:user => @user, :state => "authorized",
|
67
|
+
:product => @ipad)
|
68
|
+
@captured = Order.create(:user => @user, :state => "captured",
|
69
|
+
:product => @ipad)
|
81
70
|
end
|
82
71
|
|
83
72
|
# Now let's try and grab all pending orders, and also pending
|
@@ -85,23 +74,31 @@ end
|
|
85
74
|
test "finding pending orders" do
|
86
75
|
assert @user.orders.find(state: "pending").include?(@pending)
|
87
76
|
|
88
|
-
assert @user.orders.find(state
|
89
|
-
product_id
|
77
|
+
assert @user.orders.find(:state => "pending",
|
78
|
+
:product_id => @ipod.id).include?(@pending)
|
90
79
|
|
91
|
-
assert @user.orders.find(state
|
80
|
+
assert @user.orders.find(:state => "pending",
|
81
|
+
:product_id => @ipad.id).empty?
|
92
82
|
end
|
93
83
|
|
94
|
-
# Now we try and find captured and authorized orders.
|
95
|
-
#
|
96
|
-
#
|
84
|
+
# Now we try and find captured and authorized orders. The tricky part
|
85
|
+
# is trying to find an order that is either *captured* or *authorized*,
|
86
|
+
# since `Ohm` as of this writing doesn't support unions in its
|
87
|
+
# finder syntax.
|
97
88
|
test "finding authorized and/or captured orders" do
|
98
|
-
assert @user.orders.find(state
|
99
|
-
assert @user.orders.find(state
|
89
|
+
assert @user.orders.find(:state => "authorized").include?(@authorized)
|
90
|
+
assert @user.orders.find(:state => "captured").include?(@captured)
|
100
91
|
|
101
|
-
|
92
|
+
assert @user.orders.find(:state => ["authorized", "captured"]).empty?
|
102
93
|
|
103
|
-
|
104
|
-
|
94
|
+
auth_or_capt = @user.orders.key.volatile[:auth_or_capt]
|
95
|
+
auth_or_capt.sunionstore(
|
96
|
+
@user.orders.find(:state => "authorized").key,
|
97
|
+
@user.orders.find(:state => "captured").key
|
98
|
+
)
|
99
|
+
|
100
|
+
assert auth_or_capt.smembers.include?(@authorized.id)
|
101
|
+
assert auth_or_capt.smembers.include?(@captured.id)
|
105
102
|
end
|
106
103
|
|
107
104
|
#### Creating shortcuts
|
@@ -109,11 +106,11 @@ end
|
|
109
106
|
# You can of course define methods to make that code more readable.
|
110
107
|
class User < Ohm::Model
|
111
108
|
def authorized_orders
|
112
|
-
orders.find(state
|
109
|
+
orders.find(:state => "authorized")
|
113
110
|
end
|
114
111
|
|
115
112
|
def captured_orders
|
116
|
-
orders.find(state
|
113
|
+
orders.find(:state => "captured")
|
117
114
|
end
|
118
115
|
end
|
119
116
|
|
@@ -128,33 +125,29 @@ end
|
|
128
125
|
|
129
126
|
#### Chaining Kung-Fu
|
130
127
|
|
131
|
-
# The `Ohm::Set` takes a *Redis* key
|
132
|
-
#
|
128
|
+
# The `Ohm::Model::Set` takes a *Redis* key and a *class monad*
|
129
|
+
# for its arguments.
|
133
130
|
#
|
134
|
-
# We can simply subclass it and define the
|
135
|
-
# so we don't have to manually set
|
136
|
-
class UserOrders < Ohm::Set
|
137
|
-
attr :model
|
138
|
-
|
131
|
+
# We can simply subclass it and define the monad to always be an
|
132
|
+
# `Order` so we don't have to manually set it everytime.
|
133
|
+
class UserOrders < Ohm::Model::Set
|
139
134
|
def initialize(key)
|
140
|
-
|
141
|
-
|
142
|
-
super(key, key, @model)
|
135
|
+
super key, Ohm::Model::Wrapper.wrap(Order)
|
143
136
|
end
|
144
137
|
|
145
138
|
# Here is the crux of the chaining pattern. Instead of
|
146
|
-
# just doing a straight up `find(state
|
139
|
+
# just doing a straight up `find(:state => "pending")`, we return
|
147
140
|
# `UserOrders` again.
|
148
141
|
def pending
|
149
|
-
self.class.new(model.
|
142
|
+
self.class.new(model.index_key_for(:state, "pending"))
|
150
143
|
end
|
151
144
|
|
152
145
|
def authorized
|
153
|
-
self.class.new(model.
|
146
|
+
self.class.new(model.index_key_for(:state, "authorized"))
|
154
147
|
end
|
155
148
|
|
156
149
|
def captured
|
157
|
-
self.class.new(model.
|
150
|
+
self.class.new(model.index_key_for(:state, "captured"))
|
158
151
|
end
|
159
152
|
|
160
153
|
# Now we wrap the implementation of doing an `SUNIONSTORE` and also
|
@@ -163,24 +156,27 @@ class UserOrders < Ohm::Set
|
|
163
156
|
# NOTE: `volatile` just returns the key prepended with a `~:`, so in
|
164
157
|
# this case it would be `~:Order:accepted`.
|
165
158
|
def accepted
|
166
|
-
|
159
|
+
model.key.volatile[:accepted].sunionstore(
|
160
|
+
authorized.key, captured.key
|
161
|
+
)
|
162
|
+
|
163
|
+
self.class.new(model.key.volatile[:accepted])
|
167
164
|
end
|
168
165
|
end
|
169
166
|
|
170
167
|
# Now let's re-open the `User` class and add a customized `orders` method.
|
171
168
|
class User < Ohm::Model
|
172
169
|
def orders
|
173
|
-
UserOrders.new(Order.
|
170
|
+
UserOrders.new(Order.index_key_for(:user_id, id))
|
174
171
|
end
|
175
172
|
end
|
176
173
|
|
177
174
|
# Ok! Let's put all of that chaining code to good use.
|
178
175
|
test "finding pending orders using a chainable style" do
|
179
176
|
assert @user.orders.pending.include?(@pending)
|
177
|
+
assert @user.orders.pending.find(:product_id => @ipod.id).include?(@pending)
|
180
178
|
|
181
|
-
assert @user.orders.pending.find(product_id
|
182
|
-
|
183
|
-
assert @user.orders.pending.find(product_id: @ipad.id).empty?
|
179
|
+
assert @user.orders.pending.find(:product_id => @ipad.id).empty?
|
184
180
|
end
|
185
181
|
|
186
182
|
test "finding authorized and/or captured orders using a chainable style" do
|
@@ -192,8 +188,8 @@ test "finding authorized and/or captured orders using a chainable style" do
|
|
192
188
|
|
193
189
|
accepted = @user.orders.accepted
|
194
190
|
|
195
|
-
assert accepted.find(product_id
|
196
|
-
assert accepted.find(product_id
|
191
|
+
assert accepted.find(:product_id => @ipad.id).include?(@authorized)
|
192
|
+
assert accepted.find(:product_id => @ipad.id).include?(@captured)
|
197
193
|
end
|
198
194
|
|
199
195
|
#### Conclusion
|
data/lib/ohm.rb
CHANGED
@@ -1,13 +1,15 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
|
3
|
-
require "
|
4
|
-
require "
|
3
|
+
require "msgpack"
|
4
|
+
require "nido"
|
5
|
+
require "redic"
|
5
6
|
require "securerandom"
|
6
|
-
require "scrivener"
|
7
|
-
require "ohm/transaction"
|
8
7
|
require "ohm/command"
|
9
8
|
|
10
9
|
module Ohm
|
10
|
+
LUA_CACHE = Hash.new { |h, k| h[k] = Hash.new }
|
11
|
+
LUA_SAVE = File.expand_path("../ohm/lua/save.lua", __FILE__)
|
12
|
+
LUA_DELETE = File.expand_path("../ohm/lua/delete.lua", __FILE__)
|
11
13
|
|
12
14
|
# All of the known errors in Ohm can be traced back to one of these
|
13
15
|
# exceptions.
|
@@ -68,76 +70,42 @@ module Ohm
|
|
68
70
|
end
|
69
71
|
end
|
70
72
|
|
71
|
-
|
72
|
-
|
73
|
-
dict
|
74
|
-
end
|
75
|
-
else
|
76
|
-
def self.dict(arr)
|
77
|
-
Hash[*arr]
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
class Connection
|
83
|
-
attr_accessor :context
|
84
|
-
attr_accessor :options
|
85
|
-
|
86
|
-
def initialize(context = :main, options = {})
|
87
|
-
@context = context
|
88
|
-
@options = options
|
89
|
-
end
|
90
|
-
|
91
|
-
def reset!
|
92
|
-
threaded[context] = nil
|
73
|
+
def self.dict(arr)
|
74
|
+
Hash[*arr]
|
93
75
|
end
|
94
76
|
|
95
|
-
def
|
96
|
-
|
97
|
-
self.reset!
|
98
|
-
end
|
77
|
+
def self.sort(redis, key, options)
|
78
|
+
args = []
|
99
79
|
|
100
|
-
|
101
|
-
|
102
|
-
|
80
|
+
args.concat(["BY", options[:by]]) if options[:by]
|
81
|
+
args.concat(["GET", options[:get]]) if options[:get]
|
82
|
+
args.concat(["LIMIT"] + options[:limit]) if options[:limit]
|
83
|
+
args.concat(options[:order].split(" ")) if options[:order]
|
84
|
+
args.concat(["STORE", options[:store]]) if options[:store]
|
103
85
|
|
104
|
-
|
105
|
-
Thread.current[:ohm] ||= {}
|
86
|
+
redis.call("SORT", key, *args)
|
106
87
|
end
|
107
88
|
end
|
108
89
|
|
109
|
-
def self.conn
|
110
|
-
@conn ||= Connection.new
|
111
|
-
end
|
112
|
-
|
113
|
-
# Stores the connection options for the Redis instance.
|
114
|
-
#
|
115
|
-
# Examples:
|
116
|
-
#
|
117
|
-
# Ohm.connect(:port => 6380, :db => 1, :host => "10.0.1.1")
|
118
|
-
# Ohm.connect(:url => "redis://10.0.1.1:6380/1")
|
119
|
-
#
|
120
|
-
# All of the options are simply passed on to `Redis.connect`.
|
121
|
-
#
|
122
|
-
def self.connect(options = {})
|
123
|
-
conn.start(options)
|
124
|
-
end
|
125
|
-
|
126
90
|
# Use this if you want to do quick ad hoc redis commands against the
|
127
91
|
# defined Ohm connection.
|
128
92
|
#
|
129
93
|
# Examples:
|
130
94
|
#
|
131
|
-
# Ohm.redis.
|
132
|
-
# Ohm.redis.
|
95
|
+
# Ohm.redis.call("SET", "foo", "bar")
|
96
|
+
# Ohm.redis.call("FLUSH")
|
133
97
|
#
|
134
98
|
def self.redis
|
135
|
-
|
99
|
+
@redis ||= Redic.new
|
136
100
|
end
|
137
101
|
|
138
|
-
|
102
|
+
def self.redis=(redis)
|
103
|
+
@redis = redis
|
104
|
+
end
|
105
|
+
|
106
|
+
# Wrapper for Ohm.redis.call("FLUSHDB").
|
139
107
|
def self.flush
|
140
|
-
redis.
|
108
|
+
redis.call("FLUSHDB")
|
141
109
|
end
|
142
110
|
|
143
111
|
module Collection
|
@@ -164,19 +132,19 @@ module Ohm
|
|
164
132
|
|
165
133
|
# Wraps the whole pipelining functionality.
|
166
134
|
def fetch(ids)
|
167
|
-
|
168
|
-
|
135
|
+
ids.each do |id|
|
136
|
+
redis.queue("HGETALL", namespace[id])
|
169
137
|
end
|
170
138
|
|
171
|
-
|
139
|
+
data = redis.commit
|
172
140
|
|
173
|
-
return
|
141
|
+
return [] if data.nil?
|
174
142
|
|
175
|
-
|
176
|
-
|
143
|
+
[].tap do |result|
|
144
|
+
data.each_with_index do |atts, idx|
|
145
|
+
result << model.new(Utils.dict(atts).update(:id => ids[idx]))
|
146
|
+
end
|
177
147
|
end
|
178
|
-
|
179
|
-
res
|
180
148
|
end
|
181
149
|
end
|
182
150
|
|
@@ -195,18 +163,18 @@ module Ohm
|
|
195
163
|
|
196
164
|
# Returns the total size of the list using LLEN.
|
197
165
|
def size
|
198
|
-
|
166
|
+
redis.call("LLEN", key)
|
199
167
|
end
|
200
168
|
alias :count :size
|
201
169
|
|
202
170
|
# Returns the first element of the list using LINDEX.
|
203
171
|
def first
|
204
|
-
model[
|
172
|
+
model[redis.call("LINDEX", key, 0)]
|
205
173
|
end
|
206
174
|
|
207
175
|
# Returns the last element of the list using LINDEX.
|
208
176
|
def last
|
209
|
-
model[
|
177
|
+
model[redis.call("LINDEX", key, -1)]
|
210
178
|
end
|
211
179
|
|
212
180
|
# Checks if the model is part of this List.
|
@@ -239,20 +207,21 @@ module Ohm
|
|
239
207
|
def replace(models)
|
240
208
|
ids = models.map { |model| model.id }
|
241
209
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
210
|
+
redis.queue("MULTI")
|
211
|
+
redis.queue("DEL", key)
|
212
|
+
ids.each { |id| redis.queue("RPUSH", key, id) }
|
213
|
+
redis.queue("EXEC")
|
214
|
+
redis.commit
|
246
215
|
end
|
247
216
|
|
248
217
|
# Pushes the model to the _end_ of the list using RPUSH.
|
249
218
|
def push(model)
|
250
|
-
|
219
|
+
redis.call("RPUSH", key, model.id)
|
251
220
|
end
|
252
221
|
|
253
222
|
# Pushes the model to the _beginning_ of the list using LPUSH.
|
254
223
|
def unshift(model)
|
255
|
-
|
224
|
+
redis.call("LPUSH", key, model.id)
|
256
225
|
end
|
257
226
|
|
258
227
|
# Delete a model from the list.
|
@@ -283,16 +252,16 @@ module Ohm
|
|
283
252
|
def delete(model)
|
284
253
|
# LREM key 0 <id> means remove all elements matching <id>
|
285
254
|
# @see http://redis.io/commands/lrem
|
286
|
-
|
255
|
+
redis.call("LREM", key, 0, model.id)
|
287
256
|
end
|
288
257
|
|
289
258
|
private
|
290
259
|
def ids
|
291
|
-
|
260
|
+
redis.call("LRANGE", key, 0, -1)
|
292
261
|
end
|
293
262
|
|
294
|
-
def
|
295
|
-
model.
|
263
|
+
def redis
|
264
|
+
model.redis
|
296
265
|
end
|
297
266
|
end
|
298
267
|
|
@@ -346,10 +315,10 @@ module Ohm
|
|
346
315
|
def sort(options = {})
|
347
316
|
if options.has_key?(:get)
|
348
317
|
options[:get] = to_key(options[:get])
|
349
|
-
return execute { |key|
|
318
|
+
return execute { |key| Utils.sort(redis, key, options) }
|
350
319
|
end
|
351
320
|
|
352
|
-
fetch(execute { |key|
|
321
|
+
fetch(execute { |key| Utils.sort(redis, key, options) })
|
353
322
|
end
|
354
323
|
|
355
324
|
# Check if a model is included in this set.
|
@@ -370,7 +339,7 @@ module Ohm
|
|
370
339
|
|
371
340
|
# Returns the total size of the set using SCARD.
|
372
341
|
def size
|
373
|
-
execute { |key|
|
342
|
+
execute { |key| redis.call("SCARD", key) }
|
374
343
|
end
|
375
344
|
alias :count :size
|
376
345
|
|
@@ -398,7 +367,7 @@ module Ohm
|
|
398
367
|
|
399
368
|
# Grab all the elements of this set using SMEMBERS.
|
400
369
|
def ids
|
401
|
-
execute { |key|
|
370
|
+
execute { |key| redis.call("SMEMBERS", key) }
|
402
371
|
end
|
403
372
|
|
404
373
|
# Retrieve a specific element using an ID from this set.
|
@@ -417,7 +386,7 @@ module Ohm
|
|
417
386
|
|
418
387
|
private
|
419
388
|
def exists?(id)
|
420
|
-
execute { |key|
|
389
|
+
execute { |key| redis.call("SISMEMBER", key, id) == 1 }
|
421
390
|
end
|
422
391
|
|
423
392
|
def to_key(att)
|
@@ -467,20 +436,6 @@ module Ohm
|
|
467
436
|
MultiSet.new(namespace, model, key).except(dict)
|
468
437
|
end
|
469
438
|
|
470
|
-
# Perform an intersection between the existent set and
|
471
|
-
# the new set created by the union of the passed filters.
|
472
|
-
#
|
473
|
-
# Example:
|
474
|
-
#
|
475
|
-
# set = User.find(:status => "active")
|
476
|
-
# set.combine(:name => ["John", "Jane"])
|
477
|
-
#
|
478
|
-
# # The result will include all users with active status
|
479
|
-
# # and with names "John" or "Jane".
|
480
|
-
def combine(dict)
|
481
|
-
MultiSet.new(namespace, model, key).combine(dict)
|
482
|
-
end
|
483
|
-
|
484
439
|
# Do a union to the existing set using any number of filters.
|
485
440
|
#
|
486
441
|
# Example:
|
@@ -500,8 +455,8 @@ module Ohm
|
|
500
455
|
yield key
|
501
456
|
end
|
502
457
|
|
503
|
-
def
|
504
|
-
model.
|
458
|
+
def redis
|
459
|
+
model.redis
|
505
460
|
end
|
506
461
|
end
|
507
462
|
|
@@ -516,7 +471,7 @@ module Ohm
|
|
516
471
|
# user.posts.add(post)
|
517
472
|
#
|
518
473
|
def add(model)
|
519
|
-
|
474
|
+
redis.call("SADD", key, model.id)
|
520
475
|
end
|
521
476
|
|
522
477
|
alias_method :<<, :add
|
@@ -531,7 +486,7 @@ module Ohm
|
|
531
486
|
# user.posts.delete(post)
|
532
487
|
#
|
533
488
|
def delete(model)
|
534
|
-
|
489
|
+
redis.call("SREM", key, model.id)
|
535
490
|
end
|
536
491
|
|
537
492
|
# Replace all the existing elements of a set with a different
|
@@ -553,10 +508,11 @@ module Ohm
|
|
553
508
|
def replace(models)
|
554
509
|
ids = models.map { |model| model.id }
|
555
510
|
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
511
|
+
redis.queue("MULTI")
|
512
|
+
redis.queue("DEL", key)
|
513
|
+
ids.each { |id| redis.queue("SADD", key, id) }
|
514
|
+
redis.queue("EXEC")
|
515
|
+
redis.commit
|
560
516
|
end
|
561
517
|
end
|
562
518
|
|
@@ -612,23 +568,7 @@ module Ohm
|
|
612
568
|
#
|
613
569
|
def except(dict)
|
614
570
|
MultiSet.new(
|
615
|
-
namespace, model, Command[:sdiffstore, command,
|
616
|
-
)
|
617
|
-
end
|
618
|
-
|
619
|
-
# Perform an intersection between the existent set and
|
620
|
-
# the new set created by the union of the passed filters.
|
621
|
-
#
|
622
|
-
# Example:
|
623
|
-
#
|
624
|
-
# set = User.find(:status => "active")
|
625
|
-
# set.combine(:name => ["John", "Jane"])
|
626
|
-
#
|
627
|
-
# # The result will include all users with active status
|
628
|
-
# # and with names "John" or "Jane".
|
629
|
-
def combine(dict)
|
630
|
-
MultiSet.new(
|
631
|
-
namespace, model, Command[:sinterstore, command, unioned(dict)]
|
571
|
+
namespace, model, Command[:sdiffstore, command, intersected(dict)]
|
632
572
|
)
|
633
573
|
end
|
634
574
|
|
@@ -649,30 +589,25 @@ module Ohm
|
|
649
589
|
end
|
650
590
|
|
651
591
|
private
|
652
|
-
def
|
653
|
-
model.
|
592
|
+
def redis
|
593
|
+
model.redis
|
654
594
|
end
|
655
595
|
|
656
596
|
def intersected(dict)
|
657
597
|
Command[:sinterstore, *model.filters(dict)]
|
658
598
|
end
|
659
599
|
|
660
|
-
|
661
|
-
def unioned(dict)
|
662
|
-
Command[:sunionstore, *model.filters(dict)]
|
663
|
-
end
|
664
|
-
|
665
600
|
def execute
|
666
601
|
# namespace[:tmp] is where all the temp keys should be stored in.
|
667
|
-
#
|
668
|
-
|
602
|
+
# redis will be where all the commands are executed against.
|
603
|
+
response = command.call(namespace[:tmp], redis)
|
669
604
|
|
670
605
|
begin
|
671
606
|
|
672
607
|
# At this point, we have the final aggregated set, which we yield
|
673
608
|
# to the caller. the caller can do all the normal set operations,
|
674
609
|
# i.e. SCARD, SMEMBERS, etc.
|
675
|
-
yield
|
610
|
+
yield response
|
676
611
|
|
677
612
|
ensure
|
678
613
|
|
@@ -733,24 +668,12 @@ module Ohm
|
|
733
668
|
# SADD User:1:posts 1
|
734
669
|
#
|
735
670
|
class Model
|
736
|
-
|
737
|
-
|
738
|
-
def self.conn
|
739
|
-
@conn ||= Connection.new(name, Ohm.conn.options)
|
740
|
-
end
|
741
|
-
|
742
|
-
def self.connect(options)
|
743
|
-
@key = nil
|
744
|
-
@lua = nil
|
745
|
-
conn.start(options)
|
746
|
-
end
|
747
|
-
|
748
|
-
def self.db
|
749
|
-
conn.redis
|
671
|
+
def self.redis=(redis)
|
672
|
+
@redis = redis
|
750
673
|
end
|
751
674
|
|
752
|
-
def self.
|
753
|
-
@
|
675
|
+
def self.redis
|
676
|
+
@redis ||= Redic.new(Ohm.redis.url)
|
754
677
|
end
|
755
678
|
|
756
679
|
# The namespace for all the keys generated using this model.
|
@@ -763,14 +686,14 @@ module Ohm
|
|
763
686
|
# User.key.kind_of?(String)
|
764
687
|
# # => true
|
765
688
|
#
|
766
|
-
# User.key.kind_of?(
|
689
|
+
# User.key.kind_of?(Nido)
|
767
690
|
# # => true
|
768
691
|
#
|
769
|
-
# To find out more about
|
770
|
-
# http://github.com/soveran/
|
692
|
+
# To find out more about Nido, see:
|
693
|
+
# http://github.com/soveran/nido
|
771
694
|
#
|
772
695
|
def self.key
|
773
|
-
@key ||=
|
696
|
+
@key ||= Nido.new(self.name)
|
774
697
|
end
|
775
698
|
|
776
699
|
# Retrieve a record by ID.
|
@@ -803,7 +726,7 @@ module Ohm
|
|
803
726
|
|
804
727
|
# Check if the ID exists within <Model>:all.
|
805
728
|
def self.exists?(id)
|
806
|
-
|
729
|
+
redis.call("SISMEMBER", key[:all], id) == 1
|
807
730
|
end
|
808
731
|
|
809
732
|
# Find values in `unique` indices.
|
@@ -821,7 +744,7 @@ module Ohm
|
|
821
744
|
def self.with(att, val)
|
822
745
|
raise IndexNotFound unless uniques.include?(att)
|
823
746
|
|
824
|
-
id =
|
747
|
+
id = redis.call("HGET", key[:uniques][att], val)
|
825
748
|
new(:id => id).load! if id
|
826
749
|
end
|
827
750
|
|
@@ -1104,7 +1027,7 @@ module Ohm
|
|
1104
1027
|
define_method(name) do
|
1105
1028
|
return 0 if new?
|
1106
1029
|
|
1107
|
-
|
1030
|
+
redis.call("HGET", key[:counters], name).to_i
|
1108
1031
|
end
|
1109
1032
|
end
|
1110
1033
|
|
@@ -1182,7 +1105,7 @@ module Ohm
|
|
1182
1105
|
# Preload all the attributes of this model from Redis. Used
|
1183
1106
|
# internally by `Model::[]`.
|
1184
1107
|
def load!
|
1185
|
-
update_attributes(
|
1108
|
+
update_attributes(Utils.dict(redis.call("HGETALL", key))) unless new?
|
1186
1109
|
return self
|
1187
1110
|
end
|
1188
1111
|
|
@@ -1203,7 +1126,7 @@ module Ohm
|
|
1203
1126
|
# | u.get(:name) == "B"
|
1204
1127
|
#
|
1205
1128
|
def get(att)
|
1206
|
-
@attributes[att] =
|
1129
|
+
@attributes[att] = redis.call("HGET", key, att)
|
1207
1130
|
end
|
1208
1131
|
|
1209
1132
|
# Update an attribute value atomically. The best usecase for this
|
@@ -1213,7 +1136,12 @@ module Ohm
|
|
1213
1136
|
# and uniques. Use it wisely. The safe equivalent is `update`.
|
1214
1137
|
#
|
1215
1138
|
def set(att, val)
|
1216
|
-
val.to_s.empty?
|
1139
|
+
if val.to_s.empty?
|
1140
|
+
redis.call("HDEL", key, att)
|
1141
|
+
else
|
1142
|
+
redis.call("HSET", key, att, val)
|
1143
|
+
end
|
1144
|
+
|
1217
1145
|
@attributes[att] = val
|
1218
1146
|
end
|
1219
1147
|
|
@@ -1223,7 +1151,7 @@ module Ohm
|
|
1223
1151
|
|
1224
1152
|
# Increment a counter atomically. Internally uses HINCRBY.
|
1225
1153
|
def incr(att, count = 1)
|
1226
|
-
|
1154
|
+
redis.call("HINCRBY", key[:counters], att, count)
|
1227
1155
|
end
|
1228
1156
|
|
1229
1157
|
# Decrement a counter atomically. Internally uses HINCRBY.
|
@@ -1252,8 +1180,8 @@ module Ohm
|
|
1252
1180
|
@attributes
|
1253
1181
|
end
|
1254
1182
|
|
1255
|
-
# Export the ID
|
1256
|
-
#
|
1183
|
+
# Export the ID of the model. The approach of Ohm is to
|
1184
|
+
# whitelist public attributes, as opposed to exporting each
|
1257
1185
|
# (possibly sensitive) attribute.
|
1258
1186
|
#
|
1259
1187
|
# Example:
|
@@ -1283,91 +1211,56 @@ module Ohm
|
|
1283
1211
|
def to_hash
|
1284
1212
|
attrs = {}
|
1285
1213
|
attrs[:id] = id unless new?
|
1286
|
-
attrs[:errors] = errors if errors.any?
|
1287
1214
|
|
1288
1215
|
return attrs
|
1289
1216
|
end
|
1290
1217
|
|
1218
|
+
|
1291
1219
|
# Persist the model attributes and update indices and unique
|
1292
1220
|
# indices. The `counter`s and `set`s are not touched during save.
|
1293
1221
|
#
|
1294
|
-
# If the model is not valid, nil is returned. Otherwise, the
|
1295
|
-
# persisted model is returned.
|
1296
|
-
#
|
1297
1222
|
# Example:
|
1298
1223
|
#
|
1299
1224
|
# class User < Ohm::Model
|
1300
1225
|
# attribute :name
|
1301
|
-
#
|
1302
|
-
# def validate
|
1303
|
-
# assert_present :name
|
1304
|
-
# end
|
1305
1226
|
# end
|
1306
1227
|
#
|
1307
|
-
# User.new(:name => nil).save
|
1308
|
-
# # => nil
|
1309
|
-
#
|
1310
1228
|
# u = User.new(:name => "John").save
|
1311
1229
|
# u.kind_of?(User)
|
1312
1230
|
# # => true
|
1313
1231
|
#
|
1314
|
-
def save
|
1315
|
-
|
1316
|
-
|
1317
|
-
end
|
1318
|
-
|
1319
|
-
# Saves the model without checking for validity. Refer to
|
1320
|
-
# `Model#save` for more details.
|
1321
|
-
def save!
|
1322
|
-
t = __save__
|
1323
|
-
yield t if block_given?
|
1324
|
-
t.commit(db)
|
1232
|
+
def save
|
1233
|
+
indices = {}
|
1234
|
+
model.indices.each { |field| indices[field] = Array(send(field)) }
|
1325
1235
|
|
1326
|
-
|
1327
|
-
|
1236
|
+
uniques = {}
|
1237
|
+
model.uniques.each { |field| uniques[field] = send(field) }
|
1328
1238
|
|
1329
|
-
|
1330
|
-
Transaction.new do |t|
|
1331
|
-
t.watch(*_unique_keys)
|
1332
|
-
|
1333
|
-
if not new?
|
1334
|
-
t.watch(key)
|
1335
|
-
t.watch(key[:_indices]) if model.indices.any?
|
1336
|
-
t.watch(key[:_uniques]) if model.uniques.any?
|
1337
|
-
end
|
1239
|
+
_initialize_id if new?
|
1338
1240
|
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
1241
|
+
attrs = attributes.delete_if do |k, v|
|
1242
|
+
v.nil?
|
1243
|
+
end
|
1342
1244
|
|
1343
|
-
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1351
|
-
|
1352
|
-
existing_indices = _read_attributes(model.indices) if model.indices.any?
|
1353
|
-
existing_uniques = _read_attributes(model.uniques) if model.uniques.any?
|
1354
|
-
_uniques = db.hgetall(key[:_uniques])
|
1355
|
-
_indices = db.smembers(key[:_indices])
|
1356
|
-
uniques = _read_index_type(:uniques)
|
1357
|
-
indices = _read_index_type(:indices)
|
1358
|
-
end
|
1245
|
+
response = script(LUA_SAVE, 0,
|
1246
|
+
{ "name" => model.name,
|
1247
|
+
"id" => id,
|
1248
|
+
"key" => key
|
1249
|
+
}.to_msgpack,
|
1250
|
+
attrs.flatten.to_msgpack,
|
1251
|
+
indices.to_msgpack,
|
1252
|
+
uniques.to_msgpack
|
1253
|
+
)
|
1359
1254
|
|
1360
|
-
|
1361
|
-
|
1362
|
-
|
1363
|
-
|
1364
|
-
|
1365
|
-
_delete_uniques(_uniques)
|
1366
|
-
_save
|
1367
|
-
_save_indices(indices)
|
1368
|
-
_save_uniques(uniques)
|
1255
|
+
if response.is_a?(RuntimeError)
|
1256
|
+
if response.message =~ /(UniqueIndexViolation: (\w+))/
|
1257
|
+
raise UniqueIndexViolation, $1
|
1258
|
+
else
|
1259
|
+
raise response
|
1369
1260
|
end
|
1370
1261
|
end
|
1262
|
+
|
1263
|
+
return self
|
1371
1264
|
end
|
1372
1265
|
|
1373
1266
|
# Delete the model, including all the following keys:
|
@@ -1379,38 +1272,36 @@ module Ohm
|
|
1379
1272
|
# If the model has uniques or indices, they're also cleaned up.
|
1380
1273
|
#
|
1381
1274
|
def delete
|
1382
|
-
|
1383
|
-
|
1384
|
-
|
1385
|
-
|
1386
|
-
|
1387
|
-
|
1388
|
-
|
1389
|
-
|
1390
|
-
|
1391
|
-
|
1392
|
-
|
1393
|
-
|
1394
|
-
t.read do
|
1395
|
-
existing_indices = _read_attributes(model.indices) if model.indices.any?
|
1396
|
-
existing_uniques = _read_attributes(model.uniques) if model.uniques.any?
|
1397
|
-
_uniques = db.hgetall(key[:_uniques])
|
1398
|
-
_indices = db.smembers(key[:_indices])
|
1399
|
-
end
|
1275
|
+
uniques = {}
|
1276
|
+
model.uniques.each { |field| uniques[field] = send(field) }
|
1277
|
+
|
1278
|
+
script(LUA_DELETE, 0,
|
1279
|
+
{ "name" => model.name,
|
1280
|
+
"id" => id,
|
1281
|
+
"key" => key
|
1282
|
+
}.to_msgpack,
|
1283
|
+
uniques.to_msgpack,
|
1284
|
+
model.collections.to_msgpack
|
1285
|
+
)
|
1400
1286
|
|
1401
|
-
|
1402
|
-
|
1403
|
-
|
1404
|
-
|
1405
|
-
|
1406
|
-
|
1407
|
-
|
1408
|
-
db.del(key[:counters])
|
1409
|
-
db.del(key)
|
1410
|
-
end
|
1287
|
+
return self
|
1288
|
+
end
|
1289
|
+
|
1290
|
+
# Run lua scripts and cache the sha in order to improve
|
1291
|
+
# successive calls.
|
1292
|
+
def script(file, *args)
|
1293
|
+
cache = LUA_CACHE[redis.url]
|
1411
1294
|
|
1412
|
-
|
1295
|
+
if cache.key?(file)
|
1296
|
+
sha = cache[file]
|
1297
|
+
else
|
1298
|
+
src = File.read(file)
|
1299
|
+
sha = redis.call("SCRIPT", "LOAD", src)
|
1300
|
+
|
1301
|
+
cache[file] = sha
|
1413
1302
|
end
|
1303
|
+
|
1304
|
+
redis.call("EVALSHA", sha, *args)
|
1414
1305
|
end
|
1415
1306
|
|
1416
1307
|
# Update the model attributes and call save.
|
@@ -1484,125 +1375,21 @@ module Ohm
|
|
1484
1375
|
end
|
1485
1376
|
|
1486
1377
|
def self.new_id
|
1487
|
-
|
1378
|
+
redis.call("INCR", key[:id])
|
1488
1379
|
end
|
1489
1380
|
|
1490
1381
|
attr_writer :id
|
1491
1382
|
|
1492
|
-
def transaction
|
1493
|
-
txn = Transaction.new { |t| yield t }
|
1494
|
-
txn.commit(db)
|
1495
|
-
end
|
1496
|
-
|
1497
1383
|
def model
|
1498
1384
|
self.class
|
1499
1385
|
end
|
1500
1386
|
|
1501
|
-
def
|
1502
|
-
model.
|
1387
|
+
def redis
|
1388
|
+
model.redis
|
1503
1389
|
end
|
1504
1390
|
|
1505
1391
|
def _initialize_id
|
1506
1392
|
@id = model.new_id.to_s
|
1507
1393
|
end
|
1508
|
-
|
1509
|
-
def _skip_empty(atts)
|
1510
|
-
{}.tap do |ret|
|
1511
|
-
atts.each do |att, val|
|
1512
|
-
ret[att] = send(att).to_s unless val.to_s.empty?
|
1513
|
-
end
|
1514
|
-
|
1515
|
-
throw :empty if ret.empty?
|
1516
|
-
end
|
1517
|
-
end
|
1518
|
-
|
1519
|
-
def _unique_keys
|
1520
|
-
model.uniques.map { |att| model.key[:uniques][att] }
|
1521
|
-
end
|
1522
|
-
|
1523
|
-
def _save
|
1524
|
-
catch :empty do
|
1525
|
-
db.del(key)
|
1526
|
-
db.hmset(key, *_skip_empty(attributes).to_a.flatten)
|
1527
|
-
end
|
1528
|
-
end
|
1529
|
-
|
1530
|
-
def _verify_uniques
|
1531
|
-
if att = _detect_duplicate
|
1532
|
-
raise UniqueIndexViolation, "#{att} is not unique."
|
1533
|
-
end
|
1534
|
-
end
|
1535
|
-
|
1536
|
-
def _detect_duplicate
|
1537
|
-
model.uniques.detect do |att|
|
1538
|
-
id = db.hget(model.key[:uniques][att], send(att))
|
1539
|
-
id && id != self.id.to_s
|
1540
|
-
end
|
1541
|
-
end
|
1542
|
-
|
1543
|
-
def _read_index_type(type)
|
1544
|
-
{}.tap do |ret|
|
1545
|
-
model.send(type).each do |att|
|
1546
|
-
ret[att] = send(att)
|
1547
|
-
end
|
1548
|
-
end
|
1549
|
-
end
|
1550
|
-
|
1551
|
-
def _save_uniques(uniques)
|
1552
|
-
attrs = model.attributes
|
1553
|
-
|
1554
|
-
uniques.each do |att, val|
|
1555
|
-
unique = model.key[:uniques][att]
|
1556
|
-
|
1557
|
-
db.hset(unique, val, id)
|
1558
|
-
db.hset(key[:_uniques], unique, val) unless attrs.include?(att)
|
1559
|
-
end
|
1560
|
-
end
|
1561
|
-
|
1562
|
-
def _delete_uniques(uniques)
|
1563
|
-
uniques.each do |unique, val|
|
1564
|
-
db.hdel(unique, val)
|
1565
|
-
db.hdel(key[:_uniques], unique)
|
1566
|
-
end
|
1567
|
-
end
|
1568
|
-
|
1569
|
-
def _delete_existing_indices(existing)
|
1570
|
-
return unless existing
|
1571
|
-
|
1572
|
-
existing = existing.map { |key, value| model.to_indices(key, value) }
|
1573
|
-
existing.flatten!(1)
|
1574
|
-
|
1575
|
-
_delete_indices(existing)
|
1576
|
-
end
|
1577
|
-
|
1578
|
-
def _delete_existing_uniques(existing)
|
1579
|
-
return unless existing
|
1580
|
-
|
1581
|
-
_delete_uniques(existing.map { |key, value|
|
1582
|
-
[model.key[:uniques][key], value]
|
1583
|
-
})
|
1584
|
-
end
|
1585
|
-
|
1586
|
-
def _delete_indices(indices)
|
1587
|
-
indices.each do |index|
|
1588
|
-
db.srem(index, id)
|
1589
|
-
db.srem(key[:_indices], index)
|
1590
|
-
end
|
1591
|
-
end
|
1592
|
-
|
1593
|
-
def _save_indices(indices)
|
1594
|
-
attrs = model.attributes
|
1595
|
-
|
1596
|
-
indices.each do |att, val|
|
1597
|
-
model.to_indices(att, val).each do |index|
|
1598
|
-
db.sadd(index, id)
|
1599
|
-
db.sadd(key[:_indices], index) unless attrs.include?(att)
|
1600
|
-
end
|
1601
|
-
end
|
1602
|
-
end
|
1603
|
-
|
1604
|
-
def _read_attributes(attrs)
|
1605
|
-
Hash[attrs.zip(db.hmget(key, *attrs))]
|
1606
|
-
end
|
1607
1394
|
end
|
1608
1395
|
end
|