redisted 0.0.1

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.
@@ -0,0 +1,330 @@
1
+ #
2
+ #
3
+ # Uniqueness Index:
4
+ #
5
+ # index :test_int, unique: true
6
+ # >> An index named "test_int" that verifies that "test_int" is always unique.
7
+ # index :ti, unique: :test_int
8
+ # >> An index named "ti" that verifies that "test_int" is always unique.
9
+ # index :idxkey, unique: [:test_int,:provider]
10
+ # >> An index named "idxkey" that verifies that the "test_int"/"provider" pair is always unique.
11
+ #
12
+ # Filter/Sort Index:
13
+ #
14
+ # index :odd,includes: ->(elem){(elem.test_int%2)!=0}
15
+ # >> An index named "odd" that contains reference to objects who's test_int values are odd numbers.
16
+ # index :asc,order: ->(elem){elem.test_int},fields: :test_int
17
+ # >> An index named "asc" that contains references to *all* objects, sorted by test_int. (only recalculate when :test_int changes)
18
+ # index :sortedodd,includes: ->(elem){(elem.test_int%2)!=0}, order: ->(elem){elem.test_int}
19
+ # >> An index named "sortedodd" that contains reference to objects who's test_int values are odd numbers, sorted by test_int
20
+ # index :provider, match: ->(elem){(elem.provider)}
21
+ # >> Create multiple indices, one for each unique value returned by the match proc. Used for filtering.
22
+ #
23
+ #
24
+ module Redisted
25
+ class Base
26
+ private
27
+ def init_indexes
28
+ end
29
+ public
30
+ #
31
+ #
32
+ # Setup
33
+ #
34
+ #
35
+ def indices
36
+ self.class.indices
37
+ end
38
+ class << self
39
+ def indices= val
40
+ @indices=val
41
+ end
42
+ def indices
43
+ @indices||={}
44
+ @indices
45
+ end
46
+ def index name,params
47
+ raise InvalidIndex,"Index redefined: #{name}" if indices[name]
48
+ entry={
49
+ redis_key: "#{prefix}[#{name}]",
50
+ optimistic_retry: params[:optimistic_retry],
51
+ }
52
+ if params[:unique]
53
+ entry[:unique_index]=true
54
+ entry[:fields]= if params[:unique].is_a? Array
55
+ params[:unique]
56
+ elsif params[:unique].is_a? Symbol
57
+ [params[:unique]]
58
+ else
59
+ [name]
60
+ end
61
+ else
62
+ entry[:unique_index]=false
63
+ entry[:includes]=lambdafi_index(params[:includes],true)
64
+ entry[:order]=lambdafi_index(params[:order],1)
65
+ entry[:match]=lambdafi_index(params[:match],"") if params[:match]
66
+ entry[:fields]= if params[:fields].nil?
67
+ nil
68
+ elsif params[:fields].is_a? Array
69
+ params[:fields]
70
+ elsif params[:fields].is_a? Symbol
71
+ [params[:fields]]
72
+ else
73
+ nil
74
+ end
75
+ end
76
+ indices[name]=entry
77
+ end
78
+ def lambdafi_index options,default_value
79
+ lambda do |*args|
80
+ ret=nil
81
+ if options.nil?
82
+ ret=default_value
83
+ elsif options.respond_to? :call
84
+ ret=options.call(*args)
85
+ elsif options.is_a? Symbol
86
+ ret=args[0].send(options,*args)
87
+ else
88
+ ret=options # An integer or other constant value...
89
+ end
90
+ ret
91
+ end
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ #
98
+ #
99
+ # Process Index:
100
+ #
101
+ # data is a hash of name/value pairs of fields that are going to be changed. The
102
+ # key is the field name, and the value is the *new*, to be assigned, value of the
103
+ # field (not the current value).
104
+ # If the old value is needed, it must be pulled from @attributes_old. If it isn't
105
+ # in @attributes_old, then it must be read from Redis *before* proc.call is made.
106
+ # proc.call should make *set redis calls to update the proper redis keys to implement
107
+ # the desired results. It *cannot* make *get redis calls or any other redis calls that
108
+ # require a return value to be correct (this is because this is executed within a multi).
109
+ # Additionally, @attributes_value must be updated to the new value in this call.
110
+ #
111
+ #
112
+ # TODO: Needs repeat logic for optimistic locking...
113
+ def index_process data,&proc
114
+ state={data: data}
115
+ names=[]
116
+ data.each do |name,value|
117
+ names<<name
118
+ end
119
+ state[:attr_names]=names
120
+ u=uniq_index_start state
121
+ i=index_start state
122
+ @watch_list=[]
123
+ u=uniq_index_watch_setup u
124
+ i=index_watch_setup i
125
+ begin
126
+ if @watch_list.size>0
127
+ @redis.unwatch
128
+ @redis.watch *@watch_list
129
+ end
130
+ u=uniq_index_verify u
131
+ i=index_verify i
132
+ @redis.multi
133
+ begin
134
+ u=uniq_index_multi_start u
135
+ i=index_multi_start i
136
+ proc.call
137
+ u=uniq_index_multi_end u
138
+ i=index_multi_end i
139
+ @redis.exec
140
+ rescue Exception
141
+ @redis.discard
142
+ raise
143
+ end
144
+ rescue Exception
145
+ @redis.unwatch if @watch_list.size>0
146
+ raise
147
+ end
148
+ end
149
+
150
+ #
151
+ #
152
+ #
153
+ # Unique Index
154
+ #
155
+ #
156
+ # Creates Key:
157
+ #
158
+ # SET: model[index_name]
159
+ #
160
+ # Contains all id values from all instances of this model, and is used to verify uniqueness.
161
+ # Maintained during set_attr/save.
162
+ #
163
+ #
164
+ #
165
+ def uniq_index_start state
166
+ state
167
+ end
168
+ def uniq_index_watch_setup state
169
+ # Begin optimistic locking for all indices that are impacted by these changes...
170
+ indices.each do |name,index|
171
+ next if !index[:unique_index]
172
+ next if (index[:fields] & state[:attr_names]).size==0
173
+ @watch_list << index[:redis_key]
174
+ end
175
+ state
176
+ end
177
+
178
+ def uniq_index_verify state
179
+ indices.each do |idxname,index|
180
+ next if !index[:unique_index]
181
+ index[:fields].each do |fieldname|
182
+ # if we don't have it, we need to get it now...directly from redis
183
+ if @attributes_old_status[fieldname]!=:cached
184
+ @attributes_old[fieldname]=to_attr_type fieldname,@redis.hget("#{prefix}:#{@id}",fieldname)
185
+ @attributes_old_status[fieldname]=:cached
186
+ end
187
+ end
188
+ end
189
+
190
+ cmds_to_do={}
191
+ indices.each do |name,index|
192
+ next if !index[:unique_index]
193
+ next if (index[:fields] & state[:attr_names]).size==0
194
+ # Need to get the old and new index value...
195
+ old=unique_index_value(@attributes_old,index[:fields])
196
+ new=unique_index_value(@attributes_value.merge(state[:data]),index[:fields])
197
+ next if old==new
198
+ cmds_to_do[index[:redis_key]]||={old_vals: [],new_vals: []}
199
+ cmds_to_do[index[:redis_key]][:old_vals] << old if !old.nil?
200
+ cmds_to_do[index[:redis_key]][:new_vals] << new if !new.nil?
201
+ end
202
+
203
+ #
204
+ # Verify no value in the "new" list that is not in the "old" list actually exists in the index...
205
+ #
206
+ cmds_to_do.each do |redis_key,cmds|
207
+ (cmds[:new_vals]-cmds[:old_vals]).each do |chk_val|
208
+ raise ValueNotUnique, "Value #{chk_val} is not unique" if @redis.sismember(redis_key,chk_val)
209
+ end
210
+ end
211
+
212
+ state[:cmds_to_do]=cmds_to_do
213
+ state
214
+ end
215
+ def uniq_index_multi_start state
216
+ state
217
+ end
218
+ def uniq_index_multi_end state
219
+ state[:cmds_to_do].each do |redis_key,cmds|
220
+ @redis.srem(redis_key,*cmds[:old_vals]) if cmds[:old_vals].size>0
221
+ @redis.sadd(redis_key,*cmds[:new_vals]) if cmds[:new_vals].size>0
222
+ end
223
+ state
224
+ end
225
+ def unique_index_value vals,fields
226
+ ret=[]
227
+ fields.each do |field|
228
+ return nil if vals[field].nil? # If any value is nil, then the whole index is nil...
229
+ ret<<to_redis_type(field,vals[field]) # TODO: Take out ':','%' from the string...encode them instead...
230
+ end
231
+ ret.join(':')
232
+ end
233
+
234
+ #
235
+ #
236
+ #
237
+ # Filter/Sort Index
238
+ #
239
+ #
240
+ # Creates Key:
241
+ #
242
+ # SORTED SET: model[index_name]
243
+ #
244
+ # Contains all id values from instances of this model where the ":includes" proc returns true.
245
+ # If no ":includes" proc is given, then all instances are included. The score for all entries
246
+ # is the value returned by the ":order" proc. If that proc is not specified, then the score
247
+ # will all be 1.
248
+ #
249
+ #
250
+ #
251
+ def index_start state
252
+ state
253
+ end
254
+ def index_watch_setup state
255
+ indices.each do |name,index|
256
+ next if index[:unique_index]
257
+ next if (!index[:fields].nil?) and ((index[:fields] & state[:attr_names]).size==0)
258
+ @watch_list << index[:redis_key]
259
+ end
260
+ attr_old={}
261
+ attr_old_status={}
262
+ indices.each do |name,index|
263
+ next if index[:unique_index]
264
+ # See if we have a "match" field...if so, we need to cache the old values
265
+ if index[:match]
266
+ index[:fields].each do |fieldname|
267
+ # if we don't have it, we need to get it now...directly from redis
268
+ if attr_old_status[fieldname]!=:cached
269
+ attr_old[fieldname]=to_attr_type fieldname,@redis.hget("#{prefix}:#{@id}",fieldname)
270
+ attr_old_status[fieldname]=:cached
271
+ end
272
+ end
273
+ end
274
+ end
275
+ state[:attributes_old]=attr_old
276
+ state[:attributes_old_status]=attr_old_status
277
+ state
278
+ end
279
+
280
+ def index_verify state
281
+ state
282
+ end
283
+ def index_multi_start state
284
+ state
285
+ end
286
+ def index_multi_end state
287
+ indices.each do |name,index|
288
+ next if index[:unique_index]
289
+ next if (!index[:fields].nil?) and ((index[:fields] & state[:attr_names]).size==0)
290
+
291
+ if index[:match].nil?
292
+ if index[:includes].call(self)
293
+ @redis.zadd index[:redis_key],index[:order].call(self),self.id
294
+ else
295
+ @redis.zrem index[:redis_key],self.id
296
+ end
297
+ else
298
+ old=self.dup
299
+ old.internal_only_force_update_attributes(state[:attributes_old],state[:attributes_old_status])
300
+ old_match_val=index[:match].call(old)
301
+ new_match_val=index[:match].call(self)
302
+
303
+
304
+ if !old_match_val.nil? and old_match_val!=""
305
+ @redis.zrem "#{index[:redis_key]}:#{old_match_val}",self.id
306
+ end
307
+ if !new_match_val.nil? and new_match_val!=""
308
+ if index[:includes].call(self)
309
+ @redis.zadd "#{index[:redis_key]}:#{new_match_val}",index[:order].call(self),self.id
310
+ end
311
+ end
312
+
313
+
314
+
315
+
316
+ end
317
+
318
+
319
+ end
320
+ state
321
+ end
322
+
323
+ public # Unfortunately, this must be public, even though it should never be called externally
324
+ def internal_only_force_update_attributes attr,status
325
+ @attributes_value=attr
326
+ @attributes_status=status
327
+ end
328
+
329
+ end
330
+ end
@@ -0,0 +1,34 @@
1
+ module Redisted
2
+ class Base
3
+ class << self
4
+ #
5
+ # Use
6
+ #
7
+ def create attrs=nil,redis=nil
8
+ ret=new attrs,redis
9
+ ret.save
10
+ ret
11
+ end
12
+ def find ids
13
+ if ids.is_a? Array
14
+ ret=[]
15
+ ids.each do |id|
16
+ m=new
17
+ m.load id
18
+ ret << m
19
+ end
20
+ ret
21
+ else
22
+ m=new
23
+ m.load ids
24
+ m
25
+ end
26
+ end
27
+ end
28
+ def load id
29
+ @id=id
30
+ pre_cache_values if get_obj_option(:pre_cache_all) and get_obj_option(:pre_cache_all)[:when]==:create
31
+ self
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,105 @@
1
+ module Redisted
2
+ class Base
3
+ def references
4
+ self.class.references
5
+ end
6
+ class << self
7
+ def references= val
8
+ @references=val
9
+ end
10
+ def references
11
+ @references||={}
12
+ @references
13
+ end
14
+ end
15
+ private
16
+ def init_references
17
+ end
18
+
19
+ def get_reference ref,details
20
+ ret=nil
21
+ # TODO: Can this be cached???
22
+ if details[:ref_type]==:one
23
+ id=get_attr("#{details[:string]}_id".to_sym)
24
+ ret=details[:class].find(id)
25
+ elsif details[:ref_type]==:many
26
+ if details[:model_type]==:redisted
27
+ ret=details[:class].scoped
28
+ ret.internal_add_reference "#{prefix}[#{details[:string]}_id]"
29
+ else
30
+ return [] if @redis.exists "#{prefix}#[{details[:string]}_id]"
31
+ id_list=@redis.zrange "#{prefix}[#{details[:string]}_id]",0,-1
32
+ ret=details[:class].find(id_list)
33
+ end
34
+ end
35
+ ret
36
+ end
37
+ def set_reference ref,details,val
38
+ if details[:ref_type]==:one
39
+ set_attr "#{details[:string]}_id".to_sym,val.id.to_s
40
+ elsif details[:ref_type]==:many
41
+ raise "LEELEE: Not implemented yet"
42
+ end
43
+ nil
44
+ end
45
+ class << self
46
+ public
47
+
48
+
49
+
50
+ #
51
+ # Setup
52
+ #
53
+ def references_one sym,opt={}
54
+ details=name_to_class_details sym,opt[:as]
55
+ raise InvalidReference,"Reference already defined" if !references[details[:symbol]].nil?
56
+ field "#{details[:string]}_id".to_sym
57
+ details[:ref_type]=:one
58
+ references[details[:symbol]]=details
59
+ end
60
+ def references_many sym,opt={}
61
+ details=name_to_class_details sym,opt[:as]
62
+ raise InvalidReference,"Reference already defined" if !references[details[:plural][:symbol]].nil?
63
+ field "#{details[:plural][:string]}_list".to_sym
64
+ details[:ref_type]=:many
65
+ references[details[:plural][:symbol]]=details
66
+ end
67
+
68
+ private
69
+
70
+ def name_to_class_details obj,alt_name
71
+ if alt_name
72
+ ret={
73
+ symbol: alt_name.to_sym,
74
+ string: alt_name.to_s,
75
+ plural:{
76
+ symbol: alt_name.to_sym,
77
+ string: alt_name.to_s,
78
+ }
79
+ }
80
+ else
81
+ ret={
82
+ symbol: obj.to_sym,
83
+ string: obj.to_s,
84
+ plural:{
85
+ symbol: obj.to_s.pluralize.to_sym,
86
+ string: obj.to_s.pluralize
87
+ }
88
+ }
89
+ end
90
+ ret[:class]=if obj.kind_of? Symbol
91
+ Kernel.const_get obj.to_s.capitalize
92
+ else
93
+ Kernel.const_get obj
94
+ end
95
+ if ret[:class].respond_to?("is_redisted_model?") and ret[:class].is_redisted_model?
96
+ ret[:model_type]=:redisted
97
+ else
98
+ ret[:model_type]=:other
99
+ end
100
+ ret
101
+ end
102
+
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,190 @@
1
+ module Redisted
2
+ class Base
3
+ #
4
+ #
5
+ # Relation
6
+ #
7
+ #
8
+ class Relation
9
+ def initialize the_class,redis,scopes,indices
10
+ @the_class=the_class
11
+ @redis=redis
12
+ @scopes=scopes
13
+ @indices=indices
14
+ @is_in_list=[]
15
+ end
16
+ #
17
+ # Low Level Setup Conditions
18
+ #
19
+ def is_in index_name,*args
20
+ index=@indices[index_name.to_sym]
21
+ raise InvalidQuery,"Could not find index '#{index_name}'" if index.nil?
22
+ if index[:match]
23
+ raise InvalidQuery,"No parameter provided to index when one was expected" if args.size!=1
24
+ else
25
+ raise InvalidQuery,"Parameter provided to index when not expected" if args.size!=0
26
+ end
27
+ @is_in_list<<{:index_name=>index_name,:index=>index,:args=>args}
28
+ self
29
+ end
30
+ def internal_add_reference redis_key
31
+ @is_in_list<<{
32
+ :index_name=>nil,
33
+ :args=>nil,
34
+ :index=>{
35
+ :redis_key=>redis_key
36
+ }
37
+ }
38
+ end
39
+
40
+
41
+
42
+ #
43
+ # Calculate Results
44
+ #
45
+ def first
46
+ key=merge_keys @is_in_list
47
+ id=nil
48
+ if key
49
+ id_list=@redis.zrange key,0,-1 # LEELEE: Performance improve...
50
+ raise InvalidQuery,"No keys" if id_list.size==0
51
+ id=id_list.first
52
+ else
53
+ keyprefix="#{prefix}:"
54
+ key_list=@redis.keys "#{keyprefix}*"
55
+ raise InvalidQuery,"No keys" if key_list.size==0
56
+ id=id_list.first[keyprefix.size..-1]
57
+ end
58
+ obj=@the_class.new
59
+ obj.load id
60
+ obj
61
+ end
62
+ def last
63
+ key=merge_keys @is_in_list
64
+ id=nil
65
+ if key
66
+ id_list=@redis.zrange key,0,-1 # LEELEE: Performance improve...
67
+ raise InvalidQuery,"No keys" if id_list.size==0
68
+ id=id_list.last
69
+ else
70
+ keyprefix="#{prefix}:"
71
+ key_list=@redis.keys "#{keyprefix}*"
72
+ raise InvalidQuery,"No keys" if key_list.size==0
73
+ id=id_list.last[keyprefix.size..-1]
74
+ end
75
+ obj=@the_class.new
76
+ obj.load id
77
+ obj
78
+ end
79
+ def all
80
+ ret=[]
81
+ key=merge_keys @is_in_list
82
+ if key
83
+ id_list=@redis.zrange key,0,-1
84
+ id_list.each do |id|
85
+ obj=@the_class.new
86
+ obj.load id
87
+ ret<<obj
88
+ end
89
+ else
90
+ keyprefix="#{prefix}:"
91
+ key_list=@redis.keys "#{keyprefix}*"
92
+ key_list.each do |key|
93
+ obj=@the_class.new
94
+ obj.load key[keyprefix.size..-1]
95
+ ret<<obj
96
+ end
97
+ end
98
+ ret
99
+ end
100
+ def each &proc
101
+ key=merge_keys @is_in_list
102
+ if key
103
+ id_list=@redis.zrange key,0,-1
104
+ id_list.each do |id|
105
+ obj=@the_class.new
106
+ obj.load id
107
+ proc.call(obj)
108
+ end
109
+ else
110
+ keyprefix="#{prefix}:"
111
+ key_list=@redis.keys "#{keyprefix}*"
112
+ key_list.each do |key|
113
+ obj=@the_class.new
114
+ obj.load key[keyprefix.size..-1]
115
+ proc.call(obj)
116
+ end
117
+ end
118
+ nil
119
+ end
120
+ def delete_all
121
+ # TODO: Don't instantiate the object for delete...do it manually...
122
+ #self.each do |m|
123
+ # m.delete
124
+ #end
125
+ end
126
+ def destroy_all
127
+ self.each do |m|
128
+ m.destroy
129
+ end
130
+ end
131
+ def method_missing(id, *args)
132
+ return add_one args[0] if id.to_s=="<<"
133
+ return @scopes[id].call(*args) if @scopes[id]
134
+ return is_in id[3..-1],*args if id[0,3]=="by_"
135
+ super
136
+ end
137
+
138
+ private
139
+ def prefix
140
+ @the_class.prefix
141
+ end
142
+ def merge_keys list
143
+ return nil if list.size==0
144
+ return key_name(list.first) if list.size==1
145
+ cnt=@redis.incr "redisted_tmp_id"
146
+ # TODO: Handle calculating if weights should be used...
147
+ tmpkey="redisted_tmp[#{cnt}]"
148
+ key_list=[]
149
+ @is_in_list.each do |index|
150
+ key_list<<key_name(index)
151
+ end
152
+ @redis.zinterstore tmpkey,key_list,{:aggregate=>:sum,:weight=>3}
153
+ @redis.expire tmpkey,3600 # TODO: Value should be configurable...
154
+ return tmpkey
155
+ end
156
+ def key_name index
157
+ ret=index[:index][:redis_key]
158
+ ret+=":#{index[:args][0]}" if index[:index][:match]
159
+ ret
160
+ end
161
+
162
+ #
163
+ #
164
+ # Add objects to a relation
165
+ #
166
+ #
167
+ def add_one val
168
+ @is_in_list.each do |index|
169
+ next if !index[:index_name].nil?
170
+ @redis.zadd index[:index][:redis_key],1,val.id
171
+ end
172
+ end
173
+
174
+ end
175
+
176
+ class << self
177
+ def scoped
178
+ Relation.new self,@@redis,scopes,indices
179
+ end
180
+ def method_missing(id,*args,&proc)
181
+ is_scoped=(id[0]=='_')
182
+ is_scoped||=[:first,:last,:all,:each,:delete_all,:destroy_all,:where,:limit,:offset,:order,:reverse_order].include? id.to_sym
183
+ is_scoped||= !scopes[id.to_sym].nil?
184
+ is_scoped||= (id[0,3]=="by_")
185
+ return scoped.send(id,*args,&proc) if is_scoped
186
+ super
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,3 @@
1
+ module Redisted
2
+ VERSION = "0.0.1"
3
+ end
data/lib/redisted.rb ADDED
@@ -0,0 +1,10 @@
1
+ require "redisted/version"
2
+ require "redisted/errors"
3
+ require "redisted/base"
4
+ require "redisted/fields"
5
+ require "redisted/instantiate"
6
+ require "redisted/getsetattr"
7
+ require "redisted/index"
8
+ require "redisted/relation"
9
+ require "redisted/references"
10
+ require "redisted/delete"
File without changes
data/log/test.log ADDED
File without changes