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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c8fb32b51fcb6f8e322f0cdf25638936febbe980
4
- data.tar.gz: 37875da64dde8f61fef65909900a9889cbf5a3c9
3
+ metadata.gz: 8e636620601ef7b7ea32c20094a7e4752f438d95
4
+ data.tar.gz: 09eabd0899085d76c4d0ddb5fc037459f2b424da
5
5
  SHA512:
6
- metadata.gz: 94ad5d4c99d9f85212f0902627aa27836bcd23a61f22cef7bcd40b6e8b855656dfcb68dc274128825a04b4f4b46a6b0c6bc4dd9d678326374f18acc51d9e5f10
7
- data.tar.gz: 6a8007f49f2e2fac0c06d22323659b9b7ec987c6a5c87fda67e77b6e181f5160f4d78db0d5ebf6b1fa38c8cd34a9833b1f9ae0ec21063e05cb8b87b37bf85e96
6
+ metadata.gz: 2f5caab8eb261e8381c1c3ea8b9c36da0cc2adb0785bcd70dc2d6d128032d783e0c50e607888fe3f99ab89c097fe0056c11f911637755a10961ac3e19d156994
7
+ data.tar.gz: 71f3247c75a69a7bc5f4452ab9bf4b70824110f6d4d439d5e1510ad7565bfd64f81ac5dd7d7354dbf136bb983e7919657e93f3b1810a8ed8ad9731941c19ab67
data/.gems CHANGED
@@ -1,4 +1,4 @@
1
- nest -v 1.1.1
2
- scrivener -v 0.0.3
3
- redis -v 3.0.1
4
- cutest -v 1.2.0.rc2
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 methods {Ohm::Model::Set#find find} and
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 active users from Argentina
465
- User.find(:country => "Argentina", :status => "active")
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, :Order
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, :User
45
- reference :product, :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: "iPod")
62
- @ipad = Product.create(name: "iPad")
61
+ @ipod = Product.create(:name => "iPod")
62
+ @ipad = Product.create(:name => "iPad")
63
63
 
64
- @pending = Order.create(
65
- user: @user,
66
- state: "pending",
67
- product: @ipod
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: "pending",
89
- product_id: @ipod.id).include?(@pending)
77
+ assert @user.orders.find(:state => "pending",
78
+ :product_id => @ipod.id).include?(@pending)
90
79
 
91
- assert @user.orders.find(state: "pending", product_id: @ipad.id).empty?
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
- # Since now `Ohm` supports unions in its finder syntax,
96
- # it's really easy to do so.
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: "authorized").include?(@authorized)
99
- assert @user.orders.find(state: "captured").include?(@captured)
89
+ assert @user.orders.find(:state => "authorized").include?(@authorized)
90
+ assert @user.orders.find(:state => "captured").include?(@captured)
100
91
 
101
- auth_or_capt = @user.orders.find(state: "authorized").union(state: "captured")
92
+ assert @user.orders.find(:state => ["authorized", "captured"]).empty?
102
93
 
103
- assert auth_or_capt.include?(@authorized)
104
- assert auth_or_capt.include?(@captured)
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: "authorized")
109
+ orders.find(:state => "authorized")
113
110
  end
114
111
 
115
112
  def captured_orders
116
- orders.find(state: "captured")
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, a *namespace* and
132
- # an *Ohm model* for its arguments.
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 arguments
135
- # so we don't have to manually set them everytime.
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
- @model = Order
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: "pending")`, we return
139
+ # just doing a straight up `find(:state => "pending")`, we return
147
140
  # `UserOrders` again.
148
141
  def pending
149
- self.class.new(model.key[:indices][:state]["pending"])
142
+ self.class.new(model.index_key_for(:state, "pending"))
150
143
  end
151
144
 
152
145
  def authorized
153
- self.class.new(model.key[:indices][:state]["authorized"])
146
+ self.class.new(model.index_key_for(:state, "authorized"))
154
147
  end
155
148
 
156
149
  def captured
157
- self.class.new(model.key[:indices][:state]["captured"])
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
- Ohm::MultiSet.new(key, @model, Ohm::Command[:sunionstore, authorized.key, captured.key])
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.key[:indices][:user_id][id])
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: @ipod.id).include?(@pending)
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: @ipad.id).include?(@authorized)
196
- assert accepted.find(product_id: @ipad.id).include?(@captured)
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 "nest"
4
- require "redis"
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
- if Redis::VERSION >= "3.0.0"
72
- def self.dict(dict)
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 start(options = {})
96
- self.options = options
97
- self.reset!
98
- end
77
+ def self.sort(redis, key, options)
78
+ args = []
99
79
 
100
- def redis
101
- threaded[context] ||= Redis.connect(options)
102
- end
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
- def threaded
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.keys("User:*")
132
- # Ohm.redis.set("foo", "bar")
95
+ # Ohm.redis.call("SET", "foo", "bar")
96
+ # Ohm.redis.call("FLUSH")
133
97
  #
134
98
  def self.redis
135
- conn.redis
99
+ @redis ||= Redic.new
136
100
  end
137
101
 
138
- # Wrapper for Ohm.redis.flushdb.
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.flushdb
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
- arr = db.pipelined do
168
- ids.each { |id| db.hgetall(namespace[id]) }
135
+ ids.each do |id|
136
+ redis.queue("HGETALL", namespace[id])
169
137
  end
170
138
 
171
- res = []
139
+ data = redis.commit
172
140
 
173
- return res if arr.nil?
141
+ return [] if data.nil?
174
142
 
175
- arr.each_with_index do |atts, idx|
176
- res << model.new(Utils.dict(atts).update(:id => ids[idx]))
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
- db.llen(key)
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[db.lindex(key, 0)]
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[db.lindex(key, -1)]
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
- model.db.multi do
243
- db.del(key)
244
- ids.each { |id| db.rpush(key, id) }
245
- end
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
- db.rpush(key, model.id)
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
- db.lpush(key, model.id)
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
- db.lrem(key, 0, model.id)
255
+ redis.call("LREM", key, 0, model.id)
287
256
  end
288
257
 
289
258
  private
290
259
  def ids
291
- db.lrange(key, 0, -1)
260
+ redis.call("LRANGE", key, 0, -1)
292
261
  end
293
262
 
294
- def db
295
- model.db
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| db.sort(key, options) }
318
+ return execute { |key| Utils.sort(redis, key, options) }
350
319
  end
351
320
 
352
- fetch(execute { |key| db.sort(key, options) })
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| db.scard(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| db.smembers(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| db.sismember(key, id) }
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 db
504
- model.db
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
- db.sadd(key, model.id)
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
- db.srem(key, model.id)
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
- key.redis.multi do
557
- db.del(key)
558
- ids.each { |id| db.sadd(key, id) }
559
- end
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, unioned(dict)]
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 db
653
- model.db
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
- # db will be where all the commands are executed against.
668
- res = command.call(namespace[:tmp], db)
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 res
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
- include Scrivener::Validations
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.lua
753
- @lua ||= Lua.new(File.join(Dir.pwd, "lua"), db)
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?(Nest)
689
+ # User.key.kind_of?(Nido)
767
690
  # # => true
768
691
  #
769
- # To find out more about Nest, see:
770
- # http://github.com/soveran/nest
692
+ # To find out more about Nido, see:
693
+ # http://github.com/soveran/nido
771
694
  #
772
695
  def self.key
773
- @key ||= Nest.new(self.name, db)
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
- db.sismember(key[:all], id)
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 = db.hget(key[:uniques][att], val)
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
- db.hget(key[:counters], name).to_i
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(db.hgetall(key)) unless new?
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] = db.hget(key, 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? ? db.hdel(key, att) : db.hset(key, att, val)
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
- db.hincrby(key[:counters], att, count)
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 and the errors of the model. The approach of Ohm
1256
- # is to whitelist public attributes, as opposed to exporting each
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(&block)
1315
- return if not valid?
1316
- save!(&block)
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
- return self
1327
- end
1236
+ uniques = {}
1237
+ model.uniques.each { |field| uniques[field] = send(field) }
1328
1238
 
1329
- def __save__
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
- t.before do
1340
- _initialize_id if new?
1341
- end
1241
+ attrs = attributes.delete_if do |k, v|
1242
+ v.nil?
1243
+ end
1342
1244
 
1343
- _uniques = nil
1344
- uniques = nil
1345
- _indices = nil
1346
- indices = nil
1347
- existing_indices = nil
1348
- existing_uniques = nil
1349
-
1350
- t.read do
1351
- _verify_uniques
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
- t.write do
1361
- db.sadd(model.key[:all], id)
1362
- _delete_existing_indices(existing_indices)
1363
- _delete_existing_uniques(existing_uniques)
1364
- _delete_indices(_indices)
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
- transaction do |t|
1383
- _uniques = nil
1384
- _indices = nil
1385
- existing_indices = nil
1386
- existing_uniques = nil
1387
-
1388
- t.watch(*_unique_keys)
1389
-
1390
- t.watch(key)
1391
- t.watch(key[:_indices]) if model.indices.any?
1392
- t.watch(key[:_uniques]) if model.uniques.any?
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
- t.write do
1402
- _delete_uniques(_uniques)
1403
- _delete_indices(_indices)
1404
- _delete_existing_uniques(existing_uniques)
1405
- _delete_existing_indices(existing_indices)
1406
- model.collections.each { |e| db.del(key[e]) }
1407
- db.srem(model.key[:all], id)
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
- yield t if block_given?
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
- db.incr(key[:id])
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 db
1502
- model.db
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