redis_orm 0.5.1 → 0.8

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.
@@ -15,6 +15,10 @@ module RedisOrm
15
15
  foreign_model.to_sym
16
16
  end
17
17
 
18
+ if options[:index]
19
+ class_variable_get(:"@@indices")[model_name] << {:name => foreign_model_name, :options => {:reference => true}}
20
+ end
21
+
18
22
  define_method foreign_model_name do
19
23
  foreign_model.to_s.camelize.constantize.find($redis.get "#{model_name}:#{@id}:#{foreign_model_name}")
20
24
  end
@@ -25,13 +29,43 @@ module RedisOrm
25
29
  # we need to store this to clear old associations later
26
30
  old_assoc = self.send(foreign_model_name)
27
31
 
32
+ reference_key = "#{model_name}:#{id}:#{foreign_model_name}"
28
33
  if assoc_with_record.nil?
29
- $redis.del("#{model_name}:#{id}:#{foreign_model_name}")
34
+ $redis.del(reference_key)
30
35
  elsif assoc_with_record.model_name == foreign_model.to_s
31
- $redis.set("#{model_name}:#{id}:#{foreign_model_name}", assoc_with_record.id)
36
+ $redis.set(reference_key, assoc_with_record.id)
37
+ set_expire_on_reference_key(reference_key)
32
38
  else
33
39
  raise TypeMismatchError
34
40
  end
41
+
42
+ # handle indices for references
43
+ self.get_indices.select{|index| index[:options][:reference]}.each do |index|
44
+ # delete old reference that points to the old associated record
45
+ if !old_assoc.nil?
46
+ prepared_index = [self.model_name, index[:name], old_assoc.id].join(':')
47
+ prepared_index.downcase! if index[:options][:case_insensitive]
48
+
49
+ if index[:options][:unique]
50
+ $redis.del(prepared_index, id)
51
+ else
52
+ $redis.zrem(prepared_index, id)
53
+ end
54
+ end
55
+
56
+ # if new associated record is nil then skip to next index (since old associated record was already unreferenced)
57
+ next if assoc_with_record.nil?
58
+
59
+ prepared_index = [self.model_name, index[:name], assoc_with_record.id].join(':')
60
+
61
+ prepared_index.downcase! if index[:options][:case_insensitive]
62
+
63
+ if index[:options][:unique]
64
+ $redis.set(prepared_index, id)
65
+ else
66
+ $redis.zadd(prepared_index, Time.now.to_f, id)
67
+ end
68
+ end
35
69
 
36
70
  if !options[:as]
37
71
  if assoc_with_record.nil?
@@ -1,10 +1,16 @@
1
- # -*- encoding: utf-8 -*-
2
-
3
1
  require 'active_support/inflector/inflections'
4
2
  require 'active_support/inflector/transliterate'
5
3
  require 'active_support/inflector/methods'
6
4
  require 'active_support/inflections'
7
5
  require 'active_support/core_ext/string/inflections'
6
+
7
+ require 'active_support/core_ext/time/acts_like'
8
+ require 'active_support/core_ext/time/calculations'
9
+ require 'active_support/core_ext/time/conversions'
10
+ #require 'active_support/core_ext/time/marshal'
11
+ require 'active_support/core_ext/time/zones'
12
+
13
+ require 'active_support/core_ext/numeric'
8
14
  require 'active_support/core_ext/time/calculations' # local_time for to_time(:local)
9
15
  require 'active_support/core_ext/string/conversions' # to_time
10
16
 
@@ -20,6 +26,9 @@ module RedisOrm
20
26
  class NotIndexFound < StandardError
21
27
  end
22
28
 
29
+ class RecordNotFound < StandardError
30
+ end
31
+
23
32
  class TypeMismatchError < StandardError
24
33
  end
25
34
 
@@ -29,7 +38,7 @@ module RedisOrm
29
38
  class Base
30
39
  include ActiveModel::Validations
31
40
  include ActiveModelBehavior
32
-
41
+ include Utils
33
42
  include Associations::HasManyHelper
34
43
 
35
44
  extend Associations::BelongsTo
@@ -39,19 +48,27 @@ module RedisOrm
39
48
  attr_accessor :persisted
40
49
 
41
50
  @@properties = Hash.new{|h,k| h[k] = []}
42
- @@indices = Hash.new{|h,k| h[k] = []} # compound indices are available too
51
+ @@indices = Hash.new{|h,k| h[k] = []} # compound indices are available too
43
52
  @@associations = Hash.new{|h,k| h[k] = []}
44
53
  @@callbacks = Hash.new{|h,k| h[k] = {}}
45
54
  @@use_uuid_as_id = {}
46
-
55
+ @@descendants = []
56
+ @@expire = Hash.new{|h,k| h[k] = {}}
57
+
47
58
  class << self
48
59
 
49
60
  def inherited(from)
50
61
  [:after_save, :before_save, :after_create, :before_create, :after_destroy, :before_destroy].each do |callback_name|
51
62
  @@callbacks[from.model_name][callback_name] = []
52
63
  end
64
+
65
+ @@descendants << from
66
+ end
67
+
68
+ def descendants
69
+ @@descendants
53
70
  end
54
-
71
+
55
72
  # *options* currently supports
56
73
  # *unique* Boolean
57
74
  # *case_insensitive* Boolean
@@ -65,22 +82,19 @@ module RedisOrm
65
82
  send(:define_method, property_name) do
66
83
  value = instance_variable_get(:"@#{property_name}")
67
84
 
68
- return value if value.nil? # we must return nil here so :default option will work when saving, otherwise it'll return "" or 0 or 0.0
69
-
70
- if Time == class_name
71
- value = begin
72
- value.to_s.to_time(:local)
73
- rescue ArgumentError => e
74
- nil
75
- end
85
+ return nil if value.nil? # we must return nil here so :default option will work when saving, otherwise it'll return "" or 0 or 0.0
86
+ if /DateTime|Time/ =~ class_name.to_s
87
+ # we're using to_datetime here because to_time doesn't manage timezone correctly
88
+ value.to_s.to_datetime rescue nil
76
89
  elsif Integer == class_name
77
- value = value.to_i
90
+ value.to_i
78
91
  elsif Float == class_name
79
- value = value.to_f
92
+ value.to_f
80
93
  elsif RedisOrm::Boolean == class_name
81
- value = ((value == "false" || value == false) ? false : true)
94
+ ((value == "false" || value == false) ? false : true)
95
+ else
96
+ value
82
97
  end
83
- value
84
98
  end
85
99
 
86
100
  send(:define_method, "#{property_name}=".to_sym) do |value|
@@ -92,7 +106,6 @@ module RedisOrm
92
106
  else
93
107
  instance_variable_set(:"@#{property_name}_changes", [value])
94
108
  end
95
-
96
109
  instance_variable_set(:"@#{property_name}", value)
97
110
  end
98
111
 
@@ -117,6 +130,10 @@ module RedisOrm
117
130
  end
118
131
  end
119
132
 
133
+ def expire(seconds, options = {})
134
+ @@expire[model_name] = {seconds: seconds, options: options}
135
+ end
136
+
120
137
  def use_uuid_as_id
121
138
  @@use_uuid_as_id[model_name] = true
122
139
  @@uuid = UUID.new
@@ -126,20 +143,29 @@ module RedisOrm
126
143
  $redis.zcard("#{model_name}:ids").to_i
127
144
  end
128
145
 
129
- def first
130
- id = $redis.zrangebyscore("#{model_name}:ids", 0, Time.now.to_f, :limit => [0, 1])
131
- id.empty? ? nil : find(id[0])
146
+ def first(options = {})
147
+ if options.empty?
148
+ id = $redis.zrangebyscore("#{model_name}:ids", 0, Time.now.to_f, :limit => [0, 1])
149
+ id.empty? ? nil : find(id[0])
150
+ else
151
+ find(:first, options)
152
+ end
132
153
  end
133
154
 
134
- def last
135
- id = $redis.zrevrangebyscore("#{model_name}:ids", Time.now.to_f, 0, :limit => [0, 1])
136
- id.empty? ? nil : find(id[0])
155
+ def last(options = {})
156
+ if options.empty?
157
+ id = $redis.zrevrangebyscore("#{model_name}:ids", Time.now.to_f, 0, :limit => [0, 1])
158
+ id.empty? ? nil : find(id[0])
159
+ else
160
+ find(:last, options)
161
+ end
137
162
  end
138
-
139
- def find_index(properties)
163
+
164
+ def find_indices(properties, options = {})
140
165
  properties.map!{|p| p.to_sym}
141
-
142
- @@indices[model_name].detect do |models_index|
166
+ method = options[:first] ? :detect : :select
167
+
168
+ @@indices[model_name].send(method) do |models_index|
143
169
  if models_index[:name].is_a?(Array) && models_index[:name].size == properties.size
144
170
  # check the elements not taking into account their order
145
171
  (models_index[:name] & properties).size == properties.size
@@ -168,36 +194,106 @@ module RedisOrm
168
194
  prepared_index
169
195
  end
170
196
 
197
+ # TODO refactor this messy function
171
198
  def all(options = {})
172
199
  limit = if options[:limit] && options[:offset]
173
200
  [options[:offset].to_i, options[:limit].to_i]
174
201
  elsif options[:limit]
175
202
  [0, options[:limit].to_i]
203
+ else
204
+ [0, -1]
176
205
  end
177
206
 
178
- if options[:conditions] && options[:conditions].is_a?(Hash)
207
+ order_max_limit = Time.now.to_f
208
+ ids_key = "#{model_name}:ids"
209
+ index = nil
210
+
211
+ prepared_index = if !options[:conditions].blank? && options[:conditions].is_a?(Hash)
179
212
  properties = options[:conditions].collect{|key, value| key}
180
- index = find_index(properties)
181
213
 
182
- raise NotIndexFound if !index
214
+ # if some condition includes object => get only the id of this object
215
+ conds = options[:conditions].inject({}) do |sum, item|
216
+ key, value = item
217
+ if value.respond_to?(:model_name)
218
+ sum.merge!({key => value.id})
219
+ else
220
+ sum.merge!({key => value})
221
+ end
222
+ end
223
+
224
+ index = find_indices(properties, {first: true})
183
225
 
184
- prepared_index = construct_prepared_index(index, options[:conditions])
226
+ raise NotIndexFound if !index
185
227
 
186
- records = []
228
+ construct_prepared_index(index, conds)
229
+ else
230
+ if options[:order] && options[:order].is_a?(Array)
231
+ model_name
232
+ else
233
+ ids_key
234
+ end
235
+ end
187
236
 
188
- if index[:options][:unique]
189
- id = $redis.get prepared_index
190
- records << model_name.to_s.camelize.constantize.find(id)
237
+ order_by_property_is_string = false
238
+
239
+ # if not array => created_at native order (in which ids were pushed to "#{model_name}:ids" set by default)
240
+ direction = if !options[:order].blank?
241
+ property = {}
242
+ dir = if options[:order].is_a?(Array)
243
+ property = @@properties[model_name].detect{|prop| prop[:name].to_s == options[:order].first.to_s}
244
+ # for String values max limit for search key could be 1.0, but for Numeric values there's actually no limit
245
+ order_max_limit = 100_000_000_000
246
+ ids_key = "#{prepared_index}:#{options[:order].first}_ids"
247
+ options[:order].size == 2 ? options[:order].last : 'asc'
248
+ else
249
+ property = @@properties[model_name].detect{|prop| prop[:name].to_s == options[:order].to_s}
250
+ ids_key = prepared_index
251
+ options[:order]
252
+ end
253
+ if property && property[:class].eql?("String") && property[:options][:sortable]
254
+ order_by_property_is_string = true
255
+ end
256
+ dir
257
+ else
258
+ ids_key = prepared_index
259
+ 'asc'
260
+ end
261
+
262
+ if order_by_property_is_string
263
+ if direction.to_s == 'desc'
264
+ ids_length = $redis.llen(ids_key)
265
+ limit = if options[:offset] && options[:limit]
266
+ [(ids_length - options[:offset].to_i - options[:limit].to_i), (ids_length - options[:offset].to_i - 1)]
267
+ elsif options[:limit]
268
+ [ids_length - options[:limit].to_i, ids_length]
269
+ elsif options[:offset]
270
+ [0, (ids_length - options[:offset].to_i - 1)]
271
+ else
272
+ [0, -1]
273
+ end
274
+ $redis.lrange(ids_key, *limit).reverse.compact.collect{|id| find(id.split(':').last)}
191
275
  else
192
- ids = $redis.zrangebyscore(prepared_index, 0, Time.now.to_f)
193
- records += model_name.to_s.camelize.constantize.find(ids)
194
- end
195
- records
276
+ limit = if options[:offset] && options[:limit]
277
+ [options[:offset].to_i, (options[:offset].to_i + options[:limit].to_i)]
278
+ elsif options[:limit]
279
+ [0, options[:limit].to_i - 1]
280
+ elsif options[:offset]
281
+ [options[:offset].to_i, -1]
282
+ else
283
+ [0, -1]
284
+ end
285
+ $redis.lrange(ids_key, *limit).compact.collect{|id| find(id.split(':').last)}
286
+ end
196
287
  else
197
- if options[:order].to_s == 'desc'
198
- $redis.zrevrangebyscore("#{model_name}:ids", Time.now.to_f, 0, :limit => limit).compact.collect{|id| find(id)}
288
+ if index && index[:options][:unique]
289
+ id = $redis.get prepared_index
290
+ model_name.to_s.camelize.constantize.find(id)
199
291
  else
200
- $redis.zrangebyscore("#{model_name}:ids", 0, Time.now.to_f, :limit => limit).compact.collect{|id| find(id)}
292
+ if direction.to_s == 'desc'
293
+ $redis.zrevrangebyscore(ids_key, order_max_limit, 0, :limit => limit).compact.collect{|id| find(id)}
294
+ else
295
+ $redis.zrangebyscore(ids_key, 0, order_max_limit, :limit => limit).compact.collect{|id| find(id)}
296
+ end
201
297
  end
202
298
  end
203
299
  end
@@ -235,6 +331,15 @@ module RedisOrm
235
331
  end
236
332
  end
237
333
 
334
+ def find!(*args)
335
+ result = find(*args)
336
+ if result.nil?
337
+ raise RecordNotFound
338
+ else
339
+ result
340
+ end
341
+ end
342
+
238
343
  def after_save(callback)
239
344
  @@callbacks[model_name][:after_save] << callback
240
345
  end
@@ -262,9 +367,21 @@ module RedisOrm
262
367
  def create(options = {})
263
368
  obj = new(options, nil, false)
264
369
  obj.save
370
+
371
+ # make possible binding related models while creating class instance
372
+ options.each do |k, v|
373
+ if @@associations[model_name].detect{|h| h[:foreign_model] == k || h[:options][:as] == k}
374
+ obj.send("#{k}=", v)
375
+ end
376
+ end
377
+
378
+ $redis.expire(obj.__redis_record_key, options[:expire_in].to_i) if !options[:expire_in].blank?
379
+
265
380
  obj
266
381
  end
267
382
 
383
+ alias :create! :create
384
+
268
385
  # dynamic finders
269
386
  def method_missing(method_name, *args, &block)
270
387
  if method_name =~ /^find_(all_)?by_(\w*)/
@@ -276,7 +393,7 @@ module RedisOrm
276
393
  properties.each_with_index do |prop, i|
277
394
  properties_hash.merge!({prop.to_sym => args[i]})
278
395
  end
279
- find_index(properties)
396
+ find_indices(properties, :first => true)
280
397
  end
281
398
 
282
399
  raise NotIndexFound if !index
@@ -314,7 +431,27 @@ module RedisOrm
314
431
  def to_a
315
432
  [self]
316
433
  end
434
+
435
+ def __redis_record_key
436
+ "#{model_name}:#{id}"
437
+ end
438
+
439
+ def set_expire_on_reference_key(key)
440
+ class_expire = @@expire[model_name]
317
441
 
442
+ # if class method *expire* was invoked and number of seconds was specified then set expiry date on the HSET record key
443
+ if class_expire[:seconds]
444
+ set_expire = true
445
+
446
+ if class_expire[:options][:if] && class_expire[:options][:if].class == Proc
447
+ # *self* here refers to the instance of class which has_one association
448
+ set_expire = class_expire[:options][:if][self] # invoking specified *:if* Proc with current record as *self*
449
+ end
450
+
451
+ $redis.expire(key, class_expire[:seconds].to_i) if set_expire
452
+ end
453
+ end
454
+
318
455
  # is called from RedisOrm::Associations::HasMany to save backlinks to saved records
319
456
  def get_associations
320
457
  @@associations[self.model_name]
@@ -328,6 +465,7 @@ module RedisOrm
328
465
  def initialize(attributes = {}, id = nil, persisted = false)
329
466
  @persisted = persisted
330
467
 
468
+ # if this model uses uuid then id is a string otherwise it should be casted to Integer class
331
469
  id = @@use_uuid_as_id[model_name] ? id : id.to_i
332
470
 
333
471
  instance_variable_set(:"@id", id) if id
@@ -340,10 +478,19 @@ module RedisOrm
340
478
  instance_variable_set :"@#{prop[:name]}_changes", []
341
479
  end
342
480
  end
343
-
481
+
482
+ # cast all attributes' keys to symbols
483
+ attributes = attributes.inject({}){|sum, el| sum.merge({el[0].to_sym => el[1]})} if attributes.is_a?(Hash)
484
+
485
+ # get all names of properties to assign only those attributes from attributes hash whose key are in prop_names
486
+ # we're not using *self.respond_to?("#{key}=".to_sym)* since *belongs_to* and other assocs could create their own methods
487
+ # with *key=* name, that in turn will mess up indices
344
488
  if attributes.is_a?(Hash) && !attributes.empty?
345
- attributes.each do |key, value|
346
- self.send("#{key}=".to_sym, value) if self.respond_to?("#{key}=".to_sym)
489
+ @@properties[model_name].each do |property|
490
+ if !(value = attributes[property[:name]]).nil? # check for nil because we want to pass falses too (and value could be 'false')
491
+ value = Marshal.load(value) if ["Array", "Hash"].include?(property[:class]) && value.is_a?(String)
492
+ self.send("#{property[:name]}=".to_sym, value)
493
+ end
347
494
  end
348
495
  end
349
496
  self
@@ -353,6 +500,31 @@ module RedisOrm
353
500
  @id
354
501
  end
355
502
 
503
+ alias :to_key :id
504
+
505
+ def to_s
506
+ inspected = "<#{model_name.capitalize} id: #{@id}, "
507
+ inspected += @@properties[model_name].inject([]) do |sum, prop|
508
+ property_value = instance_variable_get(:"@#{prop[:name]}")
509
+ property_value = '"' + property_value.to_s + '"' if prop[:class].eql?("String")
510
+ property_value = 'nil' if property_value.nil?
511
+ sum << "#{prop[:name]}: " + property_value.to_s
512
+ end.join(', ')
513
+ inspected += ">"
514
+ inspected
515
+ end
516
+
517
+ def ==(other)
518
+ raise "this object could be comparable only with object of the same class" if other.class != self.class
519
+ same = true
520
+ @@properties[model_name].each do |prop|
521
+ self_var = instance_variable_get(:"@#{prop[:name]}")
522
+ same = false if other.send(prop[:name]).to_s != self_var.to_s
523
+ end
524
+ same = false if self.id != other.id
525
+ same
526
+ end
527
+
356
528
  def persisted?
357
529
  @persisted
358
530
  end
@@ -363,86 +535,20 @@ module RedisOrm
363
535
  else
364
536
  $redis.incr("#{model_name}:id")
365
537
  end
366
- end
538
+ end
367
539
 
368
540
  def save
369
541
  return false if !valid?
370
542
 
371
- # store here initial persisted flag so we could invoke :after_create callbacks in the end of the function
543
+ _check_mismatched_types_for_values
544
+
545
+ # store here initial persisted flag so we could invoke :after_create callbacks in the end of *save* function
372
546
  was_persisted = persisted?
373
547
 
374
548
  if persisted? # then there might be old indices
375
- # check whether there's old indices exists and if yes - delete them
376
- @@properties[model_name].each do |prop|
377
- # if there were no changes for current property skip it (indices remains the same)
378
- next if ! self.send(:"#{prop[:name]}_changed?")
379
-
380
- prev_prop_value = instance_variable_get(:"@#{prop[:name]}_changes").first
381
- prop_value = instance_variable_get(:"@#{prop[:name]}")
382
-
383
- indices = @@indices[model_name].inject([]) do |sum, models_index|
384
- if models_index[:name].is_a?(Array)
385
- if models_index[:name].include?(prop[:name])
386
- sum << models_index
387
- else
388
- sum
389
- end
390
- else
391
- if models_index[:name] == prop[:name]
392
- sum << models_index
393
- else
394
- sum
395
- end
396
- end
397
- end
398
-
399
- if !indices.empty?
400
- indices.each do |index|
401
- if index[:name].is_a?(Array)
402
- keys_to_delete = if index[:name].index(prop) == 0
403
- $redis.keys "#{model_name}:#{prop[:name]}#{prev_prop_value}*"
404
- else
405
- $redis.keys "#{model_name}:*#{prop[:name]}:#{prev_prop_value}*"
406
- end
407
-
408
- keys_to_delete.each{|key| $redis.del(key)}
409
- else
410
- key_to_delete = "#{model_name}:#{prop[:name]}:#{prev_prop_value}"
411
- $redis.del key_to_delete
412
- end
413
-
414
- # also we need to delete associated records *indices*
415
- if !@@associations[model_name].empty?
416
- @@associations[model_name].each do |assoc|
417
- if :belongs_to == assoc[:type]
418
- if !self.send(assoc[:foreign_model]).nil?
419
- if index[:name].is_a?(Array)
420
- keys_to_delete = if index[:name].index(prop) == 0
421
- $redis.keys "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:#{prop[:name]}#{prev_prop_value}*"
422
- else
423
- $redis.keys "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:*#{prop[:name]}:#{prev_prop_value}*"
424
- end
425
-
426
- keys_to_delete.each{|key| $redis.del(key)}
427
- else
428
- beginning_of_the_key = "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:#{prop[:name]}:"
429
-
430
- $redis.del(beginning_of_the_key + prev_prop_value.to_s)
431
-
432
- index[:options][:unique] ? $redis.set((beginning_of_the_key + prop_value.to_s), @id) : $redis.zadd((beginning_of_the_key + prop_value.to_s), Time.now.to_f, @id)
433
- end
434
- end
435
- end
436
- end
437
- end # deleting associated records *indices*
438
-
439
- end
440
- end
441
- end
549
+ _check_indices_for_persisted # remove old indices if needed
442
550
  else # !persisted?
443
- @@callbacks[model_name][:before_create].each do |callback|
444
- self.send(callback)
445
- end
551
+ @@callbacks[model_name][:before_create].each{ |callback| self.send(callback) }
446
552
 
447
553
  @id = get_next_id
448
554
  $redis.zadd "#{model_name}:ids", Time.now.to_f, @id
@@ -450,58 +556,64 @@ module RedisOrm
450
556
  self.created_at = Time.now if respond_to? :created_at
451
557
  end
452
558
 
453
- @@callbacks[model_name][:before_save].each do |callback|
454
- self.send(callback)
455
- end
559
+ @@callbacks[model_name][:before_save].each{ |callback| self.send(callback) }
456
560
 
457
561
  # automatically update *modified_at* property if it was defined
458
562
  self.modified_at = Time.now if respond_to? :modified_at
459
563
 
460
- @@properties[model_name].each do |prop|
461
- prop_value = self.send(prop[:name].to_sym)
462
-
463
- if prop_value.nil? && !prop[:options][:default].nil?
464
- prop_value = prop[:options][:default]
465
- # set instance variable in order to properly save indexes here
466
- self.instance_variable_set(:"@#{prop[:name]}", prop[:options][:default])
467
- instance_variable_set :"@#{prop[:name]}_changes", [prop[:options][:default]]
468
- end
469
-
470
- $redis.hset("#{model_name}:#{id}", prop[:name].to_s, prop_value)
564
+ _save_to_redis # main work done here
565
+ _save_new_indices
471
566
 
472
- # reducing @#{prop[:name]}_changes array to the last value
473
- prop_changes = instance_variable_get :"@#{prop[:name]}_changes"
567
+ @@callbacks[model_name][:after_save].each{ |callback| self.send(callback) }
474
568
 
475
- if prop_changes && prop_changes.size > 2
476
- instance_variable_set :"@#{prop[:name]}_changes", [prop_changes.last]
477
- end
569
+ if ! was_persisted
570
+ @@callbacks[model_name][:after_create].each{ |callback| self.send(callback) }
478
571
  end
479
572
 
480
- # save new indices in order to sort by finders
481
- # city:name:Chicago => 1
482
- @@indices[model_name].each do |index|
483
- prepared_index = construct_prepared_index(index) # instance method not class one!
484
-
485
- if index[:options][:unique]
486
- $redis.set(prepared_index, @id)
487
- else
488
- $redis.zadd(prepared_index, Time.now.to_f, @id)
489
- end
490
- end
573
+ true # if there were no errors just return true, so *if obj.save* conditions would work
574
+ end
491
575
 
492
- @@callbacks[model_name][:after_save].each do |callback|
493
- self.send(callback)
494
- end
576
+ def find_position_to_insert(sortable_key, value)
577
+ end_index = $redis.llen(sortable_key)
495
578
 
496
- if ! was_persisted
497
- @@callbacks[model_name][:after_create].each do |callback|
498
- self.send(callback)
579
+ return 0 if end_index == 0
580
+
581
+ start_index = 0
582
+ pivot_index = end_index / 2
583
+
584
+ start_el = $redis.lindex(sortable_key, start_index)
585
+ end_el = $redis.lindex(sortable_key, end_index - 1)
586
+ pivot_el = $redis.lindex(sortable_key, pivot_index)
587
+
588
+ while start_index != end_index
589
+ # aa..ab..ac..bd <- ad
590
+ if start_el.split(':').first > value # Michael > Abe
591
+ return 0
592
+ elsif end_el.split(':').first < value # Abe < Todd
593
+ return end_el
594
+ elsif start_el.split(':').first == value # Abe == Abe
595
+ return start_el
596
+ elsif pivot_el.split(':').first == value # Todd == Todd
597
+ return pivot_el
598
+ elsif end_el.split(':').first == value
599
+ return end_el
600
+ elsif (start_el.split(':').first < value) && (pivot_el.split(':').first > value)
601
+ start_index = start_index
602
+ prev_pivot_index = pivot_index
603
+ pivot_index = start_index + ((end_index - pivot_index) / 2)
604
+ end_index = prev_pivot_index
605
+ elsif (pivot_el.split(':').first < value) && (end_el.split(':').first > value) # M < V && Y > V
606
+ start_index = pivot_index
607
+ pivot_index = pivot_index + ((end_index - pivot_index) / 2)
608
+ end_index = end_index
499
609
  end
610
+ start_el = $redis.lindex(sortable_key, start_index)
611
+ end_el = $redis.lindex(sortable_key, end_index - 1)
612
+ pivot_el = $redis.lindex(sortable_key, pivot_index)
500
613
  end
501
-
502
- true # if there were no errors just return true, so *if* conditions would work
614
+ start_el
503
615
  end
504
-
616
+
505
617
  def update_attributes(attributes)
506
618
  if attributes.is_a?(Hash)
507
619
  attributes.each do |key, value|
@@ -522,7 +634,16 @@ module RedisOrm
522
634
  end
523
635
 
524
636
  @@properties[model_name].each do |prop|
637
+ property_value = instance_variable_get(:"@#{prop[:name]}").to_s
525
638
  $redis.hdel("#{model_name}:#{@id}", prop[:name].to_s)
639
+
640
+ if prop[:options][:sortable]
641
+ if prop[:class].eql?("String")
642
+ $redis.lrem "#{model_name}:#{prop[:name]}_ids", 1, "#{property_value}:#{@id}"
643
+ else
644
+ $redis.zrem "#{model_name}:#{prop[:name]}_ids", @id
645
+ end
646
+ end
526
647
  end
527
648
 
528
649
  $redis.zrem "#{model_name}:ids", @id
@@ -531,13 +652,16 @@ module RedisOrm
531
652
  if !@@associations[model_name].empty?
532
653
  @@associations[model_name].each do |assoc|
533
654
  if :belongs_to == assoc[:type]
534
- if !self.send(assoc[:foreign_model]).nil?
655
+ # if assoc has :as option
656
+ foreign_model_name = assoc[:options][:as] ? assoc[:options][:as].to_sym : assoc[:foreign_model].to_sym
657
+
658
+ if !self.send(foreign_model_name).nil?
535
659
  @@indices[model_name].each do |index|
536
660
  keys_to_delete = if index[:name].is_a?(Array)
537
661
  full_index = index[:name].inject([]){|sum, index_part| sum << index_part}.join(':')
538
- $redis.keys "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:#{full_index}:*"
662
+ $redis.keys "#{foreign_model_name}:#{self.send(foreign_model_name).id}:#{model_name.to_s.pluralize}:#{full_index}:*"
539
663
  else
540
- ["#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:#{index[:name]}:#{self.send(index[:name])}"]
664
+ ["#{foreign_model_name}:#{self.send(foreign_model_name).id}:#{model_name.to_s.pluralize}:#{index[:name]}:#{self.send(index[:name])}"]
541
665
  end
542
666
  keys_to_delete.each do |key|
543
667
  index[:options][:unique] ? $redis.del(key) : $redis.zrem(key, @id)
@@ -618,7 +742,7 @@ module RedisOrm
618
742
 
619
743
  # remove all associated indices
620
744
  @@indices[model_name].each do |index|
621
- prepared_index = construct_prepared_index(index) # instance method not class one!
745
+ prepared_index = _construct_prepared_index(index) # instance method not class one!
622
746
 
623
747
  if index[:options][:unique]
624
748
  $redis.del(prepared_index)
@@ -634,19 +758,208 @@ module RedisOrm
634
758
  true # if there were no errors just return true, so *if* conditions would work
635
759
  end
636
760
 
637
- protected
638
- def construct_prepared_index(index)
639
- prepared_index = if index[:name].is_a?(Array) # TODO sort alphabetically
640
- index[:name].inject([model_name]) do |sum, index_part|
641
- sum += [index_part, self.instance_variable_get(:"@#{index_part}")]
642
- end.join(':')
643
- else
644
- [model_name, index[:name], self.instance_variable_get(:"@#{index[:name]}")].join(':')
761
+ protected
762
+ def _construct_prepared_index(index)
763
+ prepared_index = if index[:name].is_a?(Array) # TODO sort alphabetically
764
+ index[:name].inject([model_name]) do |sum, index_part|
765
+ sum += [index_part, self.instance_variable_get(:"@#{index_part}")]
766
+ end.join(':')
767
+ else
768
+ [model_name, index[:name], self.instance_variable_get(:"@#{index[:name]}")].join(':')
769
+ end
770
+
771
+ prepared_index.downcase! if index[:options][:case_insensitive]
772
+
773
+ prepared_index
774
+ end
775
+
776
+ def _check_mismatched_types_for_values
777
+ # an exception should be raised before all saving procedures if wrong value type is specified (especcially true for Arrays and Hashes)
778
+ @@properties[model_name].each do |prop|
779
+ prop_value = self.send(prop[:name].to_sym)
780
+
781
+ if prop_value && prop[:class] != prop_value.class.to_s && ['Array', 'Hash'].include?(prop[:class].to_s)
782
+ raise TypeMismatchError
645
783
  end
784
+ end
785
+ end
786
+
787
+ def _check_indices_for_persisted
788
+ # check whether there's old indices exists and if yes - delete them
789
+ @@properties[model_name].each do |prop|
790
+ # if there were no changes for current property skip it (indices remains the same)
791
+ next if ! self.send(:"#{prop[:name]}_changed?")
646
792
 
647
- prepared_index.downcase! if index[:options][:case_insensitive]
793
+ prev_prop_value = instance_variable_get(:"@#{prop[:name]}_changes").first
794
+ prop_value = instance_variable_get(:"@#{prop[:name]}")
795
+ # TODO DRY in destroy also
796
+ if prop[:options][:sortable]
797
+ if prop[:class].eql?("String")
798
+ $redis.lrem "#{model_name}:#{prop[:name]}_ids", 1, "#{prev_prop_value}:#{@id}"
799
+ # remove id from every indexed property
800
+ @@indices[model_name].each do |index|
801
+ $redis.lrem "#{_construct_prepared_index(index)}:#{prop[:name]}_ids", 1, "#{prop_value}:#{@id}"
802
+ end
803
+ else
804
+ $redis.zrem "#{model_name}:#{prop[:name]}_ids", @id
805
+ # remove id from every indexed property
806
+ @@indices[model_name].each do |index|
807
+ $redis.zrem "#{_construct_prepared_index(index)}:#{prop[:name]}_ids", @id
808
+ end
809
+ end
810
+ end
811
+
812
+ indices = @@indices[model_name].inject([]) do |sum, models_index|
813
+ if models_index[:name].is_a?(Array)
814
+ if models_index[:name].include?(prop[:name])
815
+ sum << models_index
816
+ else
817
+ sum
818
+ end
819
+ else
820
+ if models_index[:name] == prop[:name]
821
+ sum << models_index
822
+ else
823
+ sum
824
+ end
825
+ end
826
+ end
827
+
828
+ if !indices.empty?
829
+ indices.each do |index|
830
+ if index[:name].is_a?(Array)
831
+ keys_to_delete = if index[:name].index(prop) == 0
832
+ $redis.keys "#{model_name}:#{prop[:name]}#{prev_prop_value}*"
833
+ else
834
+ $redis.keys "#{model_name}:*#{prop[:name]}:#{prev_prop_value}*"
835
+ end
836
+
837
+ keys_to_delete.each{|key| $redis.del(key)}
838
+ else
839
+ key_to_delete = "#{model_name}:#{prop[:name]}:#{prev_prop_value}"
840
+ $redis.del key_to_delete
841
+ end
842
+
843
+ # also we need to delete associated records *indices*
844
+ if !@@associations[model_name].empty?
845
+ @@associations[model_name].each do |assoc|
846
+ if :belongs_to == assoc[:type]
847
+ # if association has :as option use it, otherwise use standard :foreign_model
848
+ foreign_model_name = assoc[:options][:as] ? assoc[:options][:as].to_sym : assoc[:foreign_model].to_sym
849
+ if !self.send(foreign_model_name).nil?
850
+ if index[:name].is_a?(Array)
851
+ keys_to_delete = if index[:name].index(prop) == 0
852
+ $redis.keys "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:#{prop[:name]}#{prev_prop_value}*"
853
+ else
854
+ $redis.keys "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:*#{prop[:name]}:#{prev_prop_value}*"
855
+ end
856
+
857
+ keys_to_delete.each{|key| $redis.del(key)}
858
+ else
859
+ beginning_of_the_key = "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:#{prop[:name]}:"
860
+
861
+ $redis.del(beginning_of_the_key + prev_prop_value.to_s)
862
+
863
+ index[:options][:unique] ? $redis.set((beginning_of_the_key + prop_value.to_s), @id) : $redis.zadd((beginning_of_the_key + prop_value.to_s), Time.now.to_f, @id)
864
+ end
865
+ end
866
+ end
867
+ end
868
+ end # deleting associated records *indices*
869
+ end
870
+ end
871
+ end
872
+ end
873
+
874
+ def _save_to_redis
875
+ @@properties[model_name].each do |prop|
876
+ prop_value = self.send(prop[:name].to_sym)
648
877
 
649
- prepared_index
878
+ if prop_value.nil? && !prop[:options][:default].nil?
879
+ prop_value = prop[:options][:default]
880
+
881
+ # cast prop_value to proper class if they are not in it
882
+ # for example 'property :wage, Float, :sortable => true, :default => 20_000' turn 20_000 to 20_000.0
883
+ if prop[:class] != prop_value.class.to_s
884
+ prop_value = case prop[:class]
885
+ when 'Time'
886
+ begin
887
+ value.to_s.to_time(:local)
888
+ rescue ArgumentError => e
889
+ nil
890
+ end
891
+ when 'Integer'
892
+ prop_value.to_i
893
+ when 'Float'
894
+ prop_value.to_f
895
+ when 'RedisOrm::Boolean'
896
+ (prop_value == "false" || prop_value == false) ? false : true
897
+ end
898
+ end
899
+
900
+ # set instance variable in order to properly save indexes here
901
+ self.instance_variable_set(:"@#{prop[:name]}", prop_value)
902
+ instance_variable_set :"@#{prop[:name]}_changes", [prop_value]
903
+ end
904
+
905
+ # serialize array- and hash-type properties
906
+ if ['Array', 'Hash'].include?(prop[:class]) && !prop_value.is_a?(String)
907
+ prop_value = Marshal.dump(prop_value)
908
+ end
909
+
910
+ #TODO put out of loop
911
+ $redis.hset(__redis_record_key, prop[:name].to_s, prop_value)
912
+
913
+ set_expire_on_reference_key(__redis_record_key)
914
+
915
+ # reducing @#{prop[:name]}_changes array to the last value
916
+ prop_changes = instance_variable_get :"@#{prop[:name]}_changes"
917
+
918
+ if prop_changes && prop_changes.size > 2
919
+ instance_variable_set :"@#{prop[:name]}_changes", [prop_changes.last]
920
+ end
921
+
922
+ # if some property need to be sortable add id of the record to the appropriate sorted set
923
+ if prop[:options][:sortable]
924
+ property_value = instance_variable_get(:"@#{prop[:name]}").to_s
925
+ if prop[:class].eql?("String")
926
+ sortable_key = "#{model_name}:#{prop[:name]}_ids"
927
+ el_or_position_to_insert = find_position_to_insert(sortable_key, property_value)
928
+ el_or_position_to_insert == 0 ? $redis.lpush(sortable_key, "#{property_value}:#{@id}") : $redis.linsert(sortable_key, "AFTER", el_or_position_to_insert, "#{property_value}:#{@id}")
929
+ # add to every indexed property
930
+ @@indices[model_name].each do |index|
931
+ sortable_key = "#{_construct_prepared_index(index)}:#{prop[:name]}_ids"
932
+ el_or_position_to_insert == 0 ? $redis.lpush(sortable_key, "#{property_value}:#{@id}") : $redis.linsert(sortable_key, "AFTER", el_or_position_to_insert, "#{property_value}:#{@id}")
933
+ end
934
+ else
935
+ score = case prop[:class]
936
+ when "Integer"; property_value.to_f
937
+ when "Float"; property_value.to_f
938
+ when "RedisOrm::Boolean"; (property_value == true ? 1.0 : 0.0)
939
+ when "Time"; property_value.to_f
940
+ end
941
+ $redis.zadd "#{model_name}:#{prop[:name]}_ids", score, @id
942
+ # add to every indexed property
943
+ @@indices[model_name].each do |index|
944
+ $redis.zadd "#{_construct_prepared_index(index)}:#{prop[:name]}_ids", score, @id
945
+ end
946
+ end
947
+ end
650
948
  end
949
+ end
950
+
951
+ def _save_new_indices
952
+ # save new indices (not *reference* onces (for example not these *belongs_to :note, :index => true*)) in order to sort by finders
953
+ # city:name:Chicago => 1
954
+ @@indices[model_name].reject{|index| index[:options][:reference]}.each do |index|
955
+ prepared_index = _construct_prepared_index(index) # instance method not class one!
956
+
957
+ if index[:options][:unique]
958
+ $redis.set(prepared_index, @id)
959
+ else
960
+ $redis.zadd(prepared_index, Time.now.to_f, @id)
961
+ end
962
+ end
963
+ end
651
964
  end
652
965
  end