redisant 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ require 'redis'
2
+
3
+ require_relative 'errors'
4
+ require_relative 'inflector'
5
+ require_relative 'attribute_builder'
6
+ require_relative 'relation_builder'
7
+ require_relative 'index_builder'
8
+ require_relative 'search_builder'
9
+ require_relative 'relations'
10
+ require_relative 'index'
11
+ require_relative 'search'
12
+ require_relative 'records'
13
+ require_relative 'query'
14
+ require_relative 'criteria'
15
+
16
+
17
+
18
+ # connect to redis db
19
+ $redis = Redis.new(host: '127.0.0.1', port: 6379)
@@ -0,0 +1,168 @@
1
+ class Query
2
+
3
+ def initialize criteria
4
+ @criteria = criteria
5
+ end
6
+
7
+ def run
8
+ raise "Count and random cannot be combined" if @criteria.count? && @criteria.random?
9
+
10
+ collect_keys
11
+ if @criteria.count?
12
+ return count
13
+ elsif @criteria.random?
14
+ random
15
+ else
16
+ fetch
17
+ end
18
+
19
+ if @criteria.criteria[:exists]
20
+ any?
21
+ elsif @criteria.ids?
22
+ flatten_single_items @ids
23
+ else
24
+ load_objects
25
+ flatten_single_items @objects
26
+ end
27
+ ensure
28
+ delete_tmp
29
+ end
30
+
31
+ private
32
+
33
+ def any?
34
+ @ids and @ids.any?
35
+ end
36
+
37
+ def collect_keys
38
+ @sub_keys = []
39
+ # start from a has_many relation?
40
+ @sub_keys << @criteria.ids_key if @criteria.get_relation || @criteria.get_conditions.empty?
41
+
42
+ # where conditions
43
+ name = @criteria.object_class.name.downcase
44
+ @criteria.get_conditions.each_pair do |k,v|
45
+ @sub_keys << "#{name}:search:#{k}:#{v}"
46
+ end
47
+
48
+ @final_key = @sub_keys.first if @sub_keys.size == 1
49
+ end
50
+
51
+ def count
52
+ if @sub_keys.size > 1
53
+ count_intersection
54
+ else
55
+ count_ids
56
+ end
57
+ end
58
+
59
+ def random
60
+ if @sub_keys.size > 1
61
+ @ids = [random_intersection]
62
+ else
63
+ @ids = [random_ids]
64
+ end
65
+ end
66
+
67
+ def fetch
68
+ if @sub_keys.size > 1
69
+ if @criteria.sort? || @criteria.limit? || @criteria.single?
70
+ store_intersection
71
+ @ids = sort_and_limit
72
+ else
73
+ @ids = fetch_intersection
74
+ end
75
+ else
76
+ @final_key = @sub_keys.first
77
+ @ids = sort_and_limit
78
+ end
79
+ end
80
+
81
+ def flatten_single_items a
82
+ if @criteria.single?
83
+ a.first
84
+ else
85
+ a
86
+ end
87
+ end
88
+
89
+ def fetch_intersection
90
+ got = $redis.sinter @sub_keys
91
+ got.map { |id| id.to_i } if got
92
+ end
93
+
94
+ def store_intersection
95
+ # combine search sets to temporary set that we can sort later
96
+ @final_key = "tmp:#{rand(36**16).to_s(36)}"
97
+ $redis.sinterstore @final_key, @sub_keys
98
+ @del = true
99
+ end
100
+
101
+ def delete_tmp
102
+ $redis.del @final_key if @del
103
+ @del = nil
104
+ end
105
+
106
+ def random_intersection
107
+ # random numbers in redis lua scripts must be seeded with an outside integer
108
+ lua = "
109
+ math.randomseed(tonumber(ARGV[1]))
110
+ local ids=redis.call('SINTER', unpack(KEYS))
111
+ return ids[ math.random(#ids) ]
112
+ "
113
+ $redis.eval(lua, @sub_keys, [rand(2**32)]).to_i
114
+ end
115
+
116
+ def count_intersection
117
+ lua = "return #redis.call('SINTER', unpack(KEYS));"
118
+ $redis.eval lua, @sub_keys
119
+ end
120
+
121
+ def count_set
122
+ $redis.scard @final_key
123
+ end
124
+
125
+ def sort_and_limit
126
+ criteria = @criteria.criteria
127
+
128
+ sort = criteria[:sort]
129
+ # use dup because string might be frozen, and we might need to modify it later
130
+ order = criteria[:order].to_s.dup || 'asc'
131
+
132
+ if criteria[:limit]
133
+ limit = [criteria[:offset] || 0, criteria[:limit]]
134
+ end
135
+
136
+ if sort
137
+ index = @criteria.object_class.find_index(sort)
138
+ type = index.type
139
+ by = "#{@criteria.object_class.name.downcase}:*:attributes->#{sort}"
140
+ by << ":float" if type == 'float'
141
+ if type == 'string'
142
+ order << ' alpha'
143
+ end
144
+ else
145
+ by = 'nosort' unless criteria[:limit]==1
146
+ end
147
+
148
+ ids = $redis.sort @final_key, limit: limit, by: by, order: order
149
+ ids.map! { |t| t.to_i } if ids
150
+ end
151
+
152
+ def count_ids
153
+ @result = $redis.scard @final_key
154
+ end
155
+
156
+ def random_ids
157
+ $redis.srandmember(@final_key).to_i
158
+ end
159
+
160
+ def random_set
161
+ $redis.srandmember(@final_key).to_i
162
+ end
163
+
164
+ def load_objects
165
+ @objects = @ids.map { |id| @criteria.object_class.load id }
166
+ end
167
+
168
+ end
@@ -0,0 +1,390 @@
1
+ require 'set'
2
+ require 'json'
3
+ require 'time'
4
+
5
+ class Record
6
+ include AttributeBuilder
7
+ include RelationBuilder
8
+ include IndexBuilder
9
+ include SearchBuilder
10
+
11
+ attr_reader :id
12
+ attr_reader :attributes
13
+ attr_reader :dirty
14
+ attr_reader :errors
15
+
16
+ def initialize attributes=nil
17
+ raise Redisant::InvalidArgument.new('Wrong arguments') unless attributes==nil or attributes.is_a? Hash
18
+ @id = attributes.delete(:id) if attributes
19
+ @attributes = stringify_attributes(attributes) || {}
20
+ @prev_attributes = {}
21
+ @dirty = @attributes.keys
22
+ setup_relations if respond_to? :setup_relations
23
+ @id_saved = false
24
+ @errors = nil
25
+ end
26
+
27
+ def class_name
28
+ self.class.name.downcase
29
+ end
30
+
31
+ def self.load id
32
+ t = self.new id:id
33
+ t.load
34
+ t
35
+ end
36
+
37
+ # query
38
+ def self.find id
39
+ raise Redisant::InvalidArgument.new("Invalid argument") unless id
40
+ return nil unless exists? id
41
+ t = self.new id:id
42
+
43
+ t.load
44
+ t
45
+ end
46
+
47
+ def self.find! id
48
+ raise Redisant::InvalidArgument.new("Not found") unless exists? id
49
+ t = self.new id:id
50
+ t.load
51
+ t
52
+ end
53
+
54
+ def self.exists? id
55
+ $redis.sismember( id_key, id )
56
+ end
57
+
58
+ def self.all options={}
59
+ sort = options.delete :sort
60
+ if sort
61
+ index = self.find_index sort.to_s
62
+ raise Redisant::InvalidArgument.new("Cannot order by #{sort}") unless index
63
+ index.objects options
64
+ else
65
+ ids.map { |id| self.find id }
66
+ end
67
+ end
68
+
69
+ def self.any?
70
+ $redis.scard( id_key ) > 0
71
+ end
72
+
73
+ def self.first attributes={}
74
+ Criteria.new(self).first attributes
75
+ end
76
+
77
+ def self.last attributes={}
78
+ Criteria.new(self).last attributes
79
+ end
80
+
81
+ def self.random
82
+ Criteria.new(self).random
83
+ end
84
+
85
+ def self.where attributes
86
+ Criteria.new(self).where attributes
87
+ end
88
+
89
+ def self.count
90
+ Criteria.new(self).count
91
+ end
92
+
93
+ def self.sort options
94
+ Criteria.new(self).sort options
95
+ end
96
+
97
+ def self.order options
98
+ Criteria.new(self).order options
99
+ end
100
+
101
+ # dirty
102
+ def dirty?
103
+ @dirty.any?
104
+ end
105
+
106
+ def dirty keys
107
+ @dirty = @dirty | [keys].flatten
108
+ end
109
+
110
+ def clean
111
+ @dirty = []
112
+ end
113
+
114
+ # crud
115
+ def self.build attributes=nil
116
+ object = self.new attributes
117
+ object.save
118
+ object
119
+ end
120
+
121
+ def destroy
122
+ destroy_relations
123
+ destroy_attributes
124
+ remove_id
125
+ end
126
+
127
+ def new?
128
+ id == nil
129
+ end
130
+
131
+ def load
132
+ load_attributes
133
+ @id_saved = true
134
+ end
135
+
136
+ def save
137
+ return false unless validate
138
+ make_unique_id
139
+ add_id
140
+ save_attributes
141
+ true
142
+ end
143
+
144
+ def save!
145
+ raise Redisant::ValidationFailed.new('Validation failed') unless save
146
+ end
147
+
148
+ def validate
149
+ @errors = nil
150
+ self.class.attributes.each_pair do |key,options|
151
+ v = attribute(key)
152
+ if options[:required]
153
+ if v==nil
154
+ @errors ||= {}
155
+ @errors[key] = "is required"
156
+ end
157
+ end
158
+ if v && options[:unique]
159
+ conditions = {}
160
+ conditions[key] = v
161
+ if self.class.where(conditions).count > 0
162
+ @errors ||= {}
163
+ @errors[key] = "must be unique"
164
+ end
165
+ end
166
+ end
167
+ @errors == nil
168
+ end
169
+
170
+ def self.destroy_all
171
+ keys = ids.map {|id| "#{self.name.downcase}:#{id}:attributes" }
172
+ keys << id_key
173
+ keys << class_key('ids:counter')
174
+ $redis.del keys if keys.any?
175
+ end
176
+
177
+ # keys
178
+ def member_key str
179
+ raise Redisant::InvalidArgument.new('Cannot make key without id') unless @id
180
+ "#{class_name}:#{@id}:#{str}"
181
+ end
182
+
183
+ def self.class_key str
184
+ "#{self.name.downcase}:#{str}"
185
+ end
186
+
187
+ # single attribute
188
+ def attribute key
189
+ @attributes[key.to_s]
190
+ end
191
+
192
+ def set_attribute key, value
193
+ if value != @attributes[key.to_s]
194
+ @attributes[key.to_s] = value
195
+ dirty [key]
196
+ end
197
+ end
198
+
199
+ def update_attribute key, value
200
+ if value != @attributes[key.to_s]
201
+ @attributes[key.to_s] = value
202
+ $redis.hset member_key('attributes'), key, value
203
+ update_search
204
+ end
205
+ end
206
+
207
+ # multiple attributes
208
+ def attributes= attributes
209
+ raise Redisant::InvalidArgument.new("Invalid arguments") unless attributes.is_a? Hash
210
+ attributes.each_pair do |key,value|
211
+ if value != @attributes[key.to_s]
212
+ @attributes[key.to_s] = value
213
+ dirty key
214
+ end
215
+ end
216
+ end
217
+
218
+ def load_attributes keys=nil
219
+ if keys
220
+ keys = keys.map { |key| key.to_s }
221
+ values = $redis.hmget(member_key('attributes'), keys)
222
+ raw = keys.zip(values).to_h
223
+ else
224
+ raw = $redis.hgetall(member_key('attributes'))
225
+ end
226
+ decoded = decode_attributes(raw)
227
+ @attributes = restore_attribute_types decoded
228
+ keep_attributes
229
+ end
230
+
231
+ def save_attributes
232
+ if dirty?
233
+ synthesize_attributes
234
+ $redis.hmset member_key('attributes'), encode_attributes
235
+ clean
236
+ end
237
+ update_search
238
+ end
239
+
240
+ def update_attributes attributes
241
+ raise Redisant::InvalidArgument.new("Invalid arguments") unless attributes.is_a? Hash
242
+ @attributes.merge! stringify_attributes(attributes)
243
+ dirty attributes.keys
244
+ save_attributes
245
+ end
246
+
247
+ def destroy_attributes
248
+ $redis.del member_key('attributes')
249
+ @attributes = nil
250
+ update_search
251
+ end
252
+
253
+ def cleanup_attributes
254
+ # delete attribues in the hash that's not in our local attributes
255
+ keys = $redis.hkeys member_key('attributes')
256
+ delete = keys - @attributes.keys
257
+ $redis.hdel member_key('attributes'), delete
258
+ end
259
+
260
+ # ids
261
+ def self.id_key
262
+ class_key 'id'
263
+ end
264
+
265
+ def make_unique_id
266
+ return if @id
267
+ #use optimistic concurrency control:
268
+ #if id is taken, try again until we succeed
269
+ while true
270
+ id = $redis.incr(self.class.class_key('ids:counter')).to_i
271
+ unless self.class.exists? id
272
+ @id = id
273
+ return
274
+ end
275
+ end
276
+ end
277
+
278
+ def self.ids
279
+ Criteria.new(self).ids
280
+ end
281
+
282
+ def add_id
283
+ raise Redisant::InvalidArgument.new('Cannot add empty id') unless @id
284
+ return if @id_saved
285
+ $redis.sadd self.class.id_key, @id.to_i
286
+ @id_saved = true
287
+ end
288
+
289
+ def remove_id
290
+ raise Redisant::InvalidArgument.new('Cannot remove empty id') unless @id
291
+ $redis.srem self.class.id_key, @id
292
+ @id = nil
293
+ @id_saved = false
294
+ end
295
+
296
+
297
+ private
298
+
299
+ # redis can only sort by string or float
300
+ # to sort by eg. Time we store float version of required attributes
301
+ def synthesize_attributes
302
+ synthesized = {}
303
+ self.class.indexes.each_pair do |name,index|
304
+ if @dirty.include? name
305
+ if index.type=='float'
306
+ # for Time objects to_f return number of seconds since epoch
307
+ key = "#{name}:float"
308
+ value = @attributes[name].to_f
309
+ @attributes[key] = value
310
+ dirty key
311
+ end
312
+ end
313
+ end
314
+ end
315
+
316
+ def encode_attributes
317
+ encoded = {}
318
+ @dirty.each do |key|
319
+ k = key.to_s
320
+ encoded[k] = @attributes[k].to_json
321
+ end
322
+ encoded.flatten
323
+ end
324
+
325
+ def decode_attributes attributes
326
+ decoded = {}
327
+ attributes.each do |pair|
328
+ if pair[1]==nil
329
+ decoded[pair[0]] = nil
330
+ else
331
+ decoded[pair[0]] = JSON.parse pair[1], quirks_mode: true
332
+ end
333
+ end
334
+ @attributes = decoded
335
+ end
336
+
337
+ def restore_attribute_types attributes
338
+ restored = {}
339
+ attributes.each_pair do |k,v|
340
+ time_match = /(.+)_at$/.match k
341
+ if time_match
342
+ restored[k] = Time.parse v
343
+ end
344
+ end
345
+ attributes.merge! restored
346
+ end
347
+
348
+ def destroy_relations
349
+ relations.values.each {|relation| relation.destroy }
350
+ @relations = nil
351
+ end
352
+
353
+ def stringify_attributes attributes
354
+ attributes.collect {|k,v| [k.to_s,v] }.to_h if attributes
355
+ end
356
+
357
+ def keep_attributes
358
+ if @attributes
359
+ @prev_attributes = @attributes.dup
360
+ else
361
+ @prev_attributes = nil
362
+ end
363
+ end
364
+
365
+ # search
366
+ def update_search
367
+ prev_keys = @prev_attributes ? @prev_attributes.keys : []
368
+ cur_keys = @attributes? @attributes.keys : []
369
+ keys = prev_keys | cur_keys
370
+ keys = keys & self.class.searches.keys # only attributes with search activated
371
+ keys.each do |k|
372
+ prev = @prev_attributes? @prev_attributes[k] : nil
373
+ cur = @attributes ? @attributes[k] : nil
374
+ if prev != cur
375
+ search = self.class.find_search k.to_s
376
+ if search
377
+ if prev && cur
378
+ search.update self, prev, cur
379
+ elsif cur
380
+ search.add self, cur
381
+ elsif prev
382
+ search.remove self, prev
383
+ end
384
+ end
385
+ end
386
+ end
387
+ keep_attributes
388
+ end
389
+
390
+ end