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