redisted 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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