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