model_set 0.10.6
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.
- data/LICENSE +20 -0
- data/README.rdoc +39 -0
- data/VERSION.yml +5 -0
- data/lib/model_set/conditioned.rb +33 -0
- data/lib/model_set/conditions.rb +103 -0
- data/lib/model_set/query.rb +132 -0
- data/lib/model_set/raw_query.rb +41 -0
- data/lib/model_set/raw_sql_query.rb +19 -0
- data/lib/model_set/set_query.rb +34 -0
- data/lib/model_set/solr_query.rb +70 -0
- data/lib/model_set/sphinx_query.rb +206 -0
- data/lib/model_set/sql_base_query.rb +52 -0
- data/lib/model_set/sql_query.rb +109 -0
- data/lib/model_set.rb +743 -0
- data/lib/multi_set.rb +67 -0
- data/test/model_set_test.rb +329 -0
- data/test/multi_set_test.rb +65 -0
- data/test/test_helper.rb +23 -0
- data/vendor/sphinx_client/README.rdoc +41 -0
- data/vendor/sphinx_client/Rakefile +21 -0
- data/vendor/sphinx_client/init.rb +1 -0
- data/vendor/sphinx_client/install.rb +5 -0
- data/vendor/sphinx_client/lib/sphinx/client.rb +1093 -0
- data/vendor/sphinx_client/lib/sphinx/request.rb +50 -0
- data/vendor/sphinx_client/lib/sphinx/response.rb +69 -0
- data/vendor/sphinx_client/lib/sphinx.rb +6 -0
- data/vendor/sphinx_client/spec/client_response_spec.rb +112 -0
- data/vendor/sphinx_client/spec/client_spec.rb +469 -0
- data/vendor/sphinx_client/spec/fixtures/default_search.php +8 -0
- data/vendor/sphinx_client/spec/fixtures/default_search_index.php +8 -0
- data/vendor/sphinx_client/spec/fixtures/excerpt_custom.php +11 -0
- data/vendor/sphinx_client/spec/fixtures/excerpt_default.php +8 -0
- data/vendor/sphinx_client/spec/fixtures/excerpt_flags.php +11 -0
- data/vendor/sphinx_client/spec/fixtures/field_weights.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter_exclude.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter_float_range.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter_float_range_exclude.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter_range.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter_range_exclude.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter_range_int64.php +10 -0
- data/vendor/sphinx_client/spec/fixtures/filter_ranges.php +10 -0
- data/vendor/sphinx_client/spec/fixtures/filters.php +10 -0
- data/vendor/sphinx_client/spec/fixtures/filters_different.php +13 -0
- data/vendor/sphinx_client/spec/fixtures/geo_anchor.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_attr.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_attrpair.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_day.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_day_sort.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_month.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_week.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_year.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_distinct.php +10 -0
- data/vendor/sphinx_client/spec/fixtures/id_range.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/id_range64.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/index_weights.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/keywords.php +8 -0
- data/vendor/sphinx_client/spec/fixtures/limits.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/limits_cutoff.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/limits_max.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/limits_max_cutoff.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_all.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_any.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_boolean.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_extended.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_extended2.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_fullscan.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_phrase.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/max_query_time.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/miltiple_queries.php +12 -0
- data/vendor/sphinx_client/spec/fixtures/ranking_bm25.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/ranking_none.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/ranking_proximity.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/ranking_proximity_bm25.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/ranking_wordcount.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/retries.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/retries_delay.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/select.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/set_override.php +11 -0
- data/vendor/sphinx_client/spec/fixtures/sort_attr_asc.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/sort_attr_desc.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/sort_expr.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/sort_extended.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/sort_relevance.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/sort_time_segments.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/sphinxapi.php +1269 -0
- data/vendor/sphinx_client/spec/fixtures/update_attributes.php +8 -0
- data/vendor/sphinx_client/spec/fixtures/update_attributes_mva.php +8 -0
- data/vendor/sphinx_client/spec/fixtures/weights.php +9 -0
- data/vendor/sphinx_client/spec/sphinx/sphinx-id64.conf +67 -0
- data/vendor/sphinx_client/spec/sphinx/sphinx.conf +67 -0
- data/vendor/sphinx_client/spec/sphinx/sphinx_test.sql +86 -0
- data/vendor/sphinx_client/sphinx.yml.tpl +3 -0
- data/vendor/sphinx_client/tasks/sphinx.rake +75 -0
- metadata +151 -0
data/lib/model_set.rb
ADDED
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'active_record'
|
|
3
|
+
require 'deep_clonable'
|
|
4
|
+
require 'ordered_set'
|
|
5
|
+
|
|
6
|
+
$:.unshift(File.dirname(__FILE__))
|
|
7
|
+
require 'multi_set'
|
|
8
|
+
require 'model_set/query'
|
|
9
|
+
require 'model_set/set_query'
|
|
10
|
+
require 'model_set/raw_query'
|
|
11
|
+
require 'model_set/conditions'
|
|
12
|
+
require 'model_set/conditioned'
|
|
13
|
+
require 'model_set/sql_base_query'
|
|
14
|
+
require 'model_set/sql_query'
|
|
15
|
+
require 'model_set/raw_sql_query'
|
|
16
|
+
require 'model_set/solr_query'
|
|
17
|
+
require 'model_set/sphinx_query'
|
|
18
|
+
|
|
19
|
+
class ModelSet
|
|
20
|
+
include Enumerable
|
|
21
|
+
include ActiveSupport::CoreExtensions::Array::Conversions
|
|
22
|
+
|
|
23
|
+
deep_clonable
|
|
24
|
+
|
|
25
|
+
MAX_CACHE_SIZE = 1000 if not defined?(MAX_CACHE_SIZE)
|
|
26
|
+
|
|
27
|
+
attr_reader :created_at
|
|
28
|
+
|
|
29
|
+
def initialize(query_or_models)
|
|
30
|
+
if query_or_models.kind_of?(Query)
|
|
31
|
+
@query = query_or_models
|
|
32
|
+
elsif query_or_models.kind_of?(self.class)
|
|
33
|
+
self.ids = query_or_models.ids
|
|
34
|
+
@models_by_id = query_or_models.models_by_id
|
|
35
|
+
elsif query_or_models
|
|
36
|
+
self.ids = as_ids(query_or_models)
|
|
37
|
+
end
|
|
38
|
+
@created_at = Time.now
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def older_than?(duration)
|
|
42
|
+
created_at.nil? or created_at < Time.now - duration
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def ids
|
|
46
|
+
model_ids.to_a
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def missing_ids
|
|
50
|
+
( @missing_ids || [] ).uniq
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
[:add!, :unshift!, :subtract!, :intersect!, :reorder!, :reverse_reorder!].each do |action|
|
|
54
|
+
define_method(action) do |models|
|
|
55
|
+
anchor!(:set)
|
|
56
|
+
query.send(action, as_ids(models))
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
clone_method :+, :add!
|
|
62
|
+
clone_method :-, :subtract!
|
|
63
|
+
clone_method :&, :intersect!
|
|
64
|
+
|
|
65
|
+
alias << add!
|
|
66
|
+
alias concat add!
|
|
67
|
+
alias delete subtract!
|
|
68
|
+
alias without! subtract!
|
|
69
|
+
clone_method :without
|
|
70
|
+
|
|
71
|
+
clone_method :shuffle
|
|
72
|
+
def shuffle!
|
|
73
|
+
reanchor!(:set)
|
|
74
|
+
query.shuffle!
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def include?(model)
|
|
79
|
+
model_id = as_id(model)
|
|
80
|
+
model_ids.include?(model_id)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def by_id(id)
|
|
84
|
+
return nil if id.nil?
|
|
85
|
+
fetch_models([id]) unless models_by_id[id]
|
|
86
|
+
models_by_id[id] || nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# FIXME make work for nested offsets
|
|
90
|
+
def [](*args)
|
|
91
|
+
case args.size
|
|
92
|
+
when 1
|
|
93
|
+
index = args[0]
|
|
94
|
+
if index.kind_of?(Range)
|
|
95
|
+
offset = index.begin
|
|
96
|
+
limit = index.end - index.begin
|
|
97
|
+
limit += 1 unless index.exclude_end?
|
|
98
|
+
self.limit(limit, offset)
|
|
99
|
+
else
|
|
100
|
+
by_id(ids[index])
|
|
101
|
+
end
|
|
102
|
+
when 2
|
|
103
|
+
offset, limit = args
|
|
104
|
+
self.limit(limit, offset)
|
|
105
|
+
else
|
|
106
|
+
raise ArgumentError.new("wrong number of arguments (#{args.size} for 1 or 2)")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
alias slice []
|
|
110
|
+
|
|
111
|
+
def first(limit=nil)
|
|
112
|
+
if limit
|
|
113
|
+
self.limit(limit)
|
|
114
|
+
else
|
|
115
|
+
self[0]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def last(limit=nil)
|
|
120
|
+
if limit
|
|
121
|
+
self.limit(limit, size - limit)
|
|
122
|
+
else
|
|
123
|
+
self[-1]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def second
|
|
128
|
+
self[1]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def in_groups_of(num)
|
|
132
|
+
each_slice(num) do |slice_set|
|
|
133
|
+
slice = slice_set.to_a
|
|
134
|
+
slice[num-1] = nil if slice.size < num
|
|
135
|
+
yield slice
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def each_slice(num=MAX_CACHE_SIZE)
|
|
140
|
+
ids.each_slice(num) do |slice_ids|
|
|
141
|
+
set = self.clone
|
|
142
|
+
set.ids = slice_ids
|
|
143
|
+
set.clear_cache!
|
|
144
|
+
yield set
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def each
|
|
149
|
+
num_models = ids.size
|
|
150
|
+
ids.each_slice(MAX_CACHE_SIZE) do |slice_ids|
|
|
151
|
+
clear_cache! if num_models > MAX_CACHE_SIZE
|
|
152
|
+
fetch_models(slice_ids)
|
|
153
|
+
slice_ids.each do |id|
|
|
154
|
+
# Skip models that aren't in the database.
|
|
155
|
+
model = models_by_id[id]
|
|
156
|
+
if model
|
|
157
|
+
yield model
|
|
158
|
+
else
|
|
159
|
+
( @missing_ids ||= [] ) << id
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def each_with_index
|
|
166
|
+
i = per_page ? (current_page - 1) * per_page : 0
|
|
167
|
+
each do |model|
|
|
168
|
+
yield(model, i)
|
|
169
|
+
i += 1
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def reject(&block)
|
|
174
|
+
self.clone.reject!(&block)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def reject!
|
|
178
|
+
filtered_ids = []
|
|
179
|
+
self.each do |model|
|
|
180
|
+
filtered_ids << model.send(id_field) unless yield model
|
|
181
|
+
end
|
|
182
|
+
self.ids = filtered_ids
|
|
183
|
+
self
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def select(limit = nil, &block)
|
|
187
|
+
self.clone.select!(limit, &block)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def select!(limit = nil)
|
|
191
|
+
filtered_ids = []
|
|
192
|
+
self.each do |model|
|
|
193
|
+
if yield model
|
|
194
|
+
filtered_ids << model.send(id_field)
|
|
195
|
+
break if filtered_ids.size == limit
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
self.ids = filtered_ids
|
|
199
|
+
self
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def reject_ids(&block)
|
|
203
|
+
self.clone.select_ids!(&block)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def reject_ids!
|
|
207
|
+
self.ids = ids.select do |id|
|
|
208
|
+
not yield id
|
|
209
|
+
end
|
|
210
|
+
self
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def select_ids(&block)
|
|
214
|
+
self.clone.select_ids!(&block)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def select_ids!
|
|
218
|
+
self.ids = ids.select do |id|
|
|
219
|
+
yield id
|
|
220
|
+
end
|
|
221
|
+
self
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def reject_raw(&block)
|
|
225
|
+
self.clone.reject_raw!(&block)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def reject_raw!(&block)
|
|
229
|
+
anchor!(:raw)
|
|
230
|
+
query.reject!(&block)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def select_raw(&block)
|
|
234
|
+
self.clone.select_raw!(&block)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def select_raw!(&block)
|
|
238
|
+
anchor!(:raw)
|
|
239
|
+
query.select!(&block)
|
|
240
|
+
self
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def sort_by_raw(&block)
|
|
244
|
+
self.clone.sort_by_raw!(&block)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def sort_by_raw!(&block)
|
|
248
|
+
anchor!(:raw)
|
|
249
|
+
query.sort_by!(&block)
|
|
250
|
+
self
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def sort(&block)
|
|
254
|
+
self.clone.sort!(&block)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def sort!(&block)
|
|
258
|
+
block ||= lambda {|a,b| a <=> b}
|
|
259
|
+
sorted_ids = to_a.sort(&block).collect {|m| m.id}
|
|
260
|
+
reorder!(sorted_ids)
|
|
261
|
+
self
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def sort_by(&block)
|
|
265
|
+
self.clone.sort_by!(&block)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def sort_by!(&block)
|
|
269
|
+
sorted_ids = to_a.sort_by(&block).collect {|m| m.id}
|
|
270
|
+
reorder!(sorted_ids)
|
|
271
|
+
self
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def partition_by(filter)
|
|
275
|
+
filter = filter.to_s
|
|
276
|
+
filter[-1] = '' if filter =~ /\!$/
|
|
277
|
+
positive = self.send(filter)
|
|
278
|
+
negative = self - positive
|
|
279
|
+
if block_given?
|
|
280
|
+
yield(positive, negative)
|
|
281
|
+
else
|
|
282
|
+
[positive, negative]
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def count
|
|
287
|
+
query.count
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def size
|
|
291
|
+
query.size
|
|
292
|
+
end
|
|
293
|
+
alias length size
|
|
294
|
+
|
|
295
|
+
def any?
|
|
296
|
+
return super if block_given?
|
|
297
|
+
return false if query.nil?
|
|
298
|
+
size > 0
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def empty?
|
|
302
|
+
not any?
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def current_page # for will_paginate
|
|
306
|
+
query.page
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def per_page # for will_paginate
|
|
310
|
+
query.limit
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def total_entries # for will_paginate
|
|
314
|
+
query.count
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def total_pages # for will_paginate
|
|
318
|
+
query.pages
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def empty!
|
|
322
|
+
self.ids = []
|
|
323
|
+
self
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def ids=(model_ids)
|
|
327
|
+
model_ids = model_ids.collect {|id| id.to_i}
|
|
328
|
+
self.query = SetQuery.new(self.class)
|
|
329
|
+
query.add!(model_ids)
|
|
330
|
+
self
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def query=(query)
|
|
334
|
+
@query = query
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
QUERY_TYPES = {
|
|
338
|
+
:set => SetQuery,
|
|
339
|
+
:sql => SQLQuery,
|
|
340
|
+
:solr => SolrQuery,
|
|
341
|
+
:sphinx => SphinxQuery,
|
|
342
|
+
:raw => RawQuery,
|
|
343
|
+
} if not defined?(QUERY_TYPES)
|
|
344
|
+
|
|
345
|
+
attr_reader :query
|
|
346
|
+
|
|
347
|
+
def query_class(type = query.class)
|
|
348
|
+
type.kind_of?(Symbol) ? QUERY_TYPES[type] : type
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def query_type?(type)
|
|
352
|
+
query_class(type) == query_class
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def anchor!(type = default_query_type, *args)
|
|
356
|
+
return unless type
|
|
357
|
+
query_class = query_class(type)
|
|
358
|
+
if not query_type?(query_class)
|
|
359
|
+
self.query = query_class.new(self, *args)
|
|
360
|
+
end
|
|
361
|
+
self
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def reanchor!(type = default_query_type, *args)
|
|
365
|
+
# Force anchoring even if you are already anchored to this type.
|
|
366
|
+
return unless type
|
|
367
|
+
self.query = query_class(type).new(self, *args)
|
|
368
|
+
self
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def default_query_type
|
|
372
|
+
:sql
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
[:add_conditions!, :add_joins!, :in!, :invert!, :order_by!].each do |method_name|
|
|
376
|
+
clone_method method_name
|
|
377
|
+
define_method(method_name) do |*args|
|
|
378
|
+
# Use the default query engine if none is specified.
|
|
379
|
+
anchor!( extract_opt(:query_type, args) || default_query_type )
|
|
380
|
+
|
|
381
|
+
query.send(method_name, *args)
|
|
382
|
+
self
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
[:unsorted!, :limit!, :page!, :unlimited!, :reverse!].each do |method_name|
|
|
387
|
+
clone_method method_name
|
|
388
|
+
define_method(method_name) do |*args|
|
|
389
|
+
# Don't change the query engine by default
|
|
390
|
+
anchor!( extract_opt(:query_type, args) )
|
|
391
|
+
|
|
392
|
+
# Use the default query engine if the the current engine doesn't respond to the method.
|
|
393
|
+
anchor!(default_query_type) unless query.respond_to?(method_name)
|
|
394
|
+
anchor!(:set) if [:limit!, :page!].include?(method_name) and not query.limit_enabled?
|
|
395
|
+
|
|
396
|
+
query.send(method_name, *args)
|
|
397
|
+
self
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def extract_opt(key, args)
|
|
402
|
+
opts = args.last.kind_of?(Hash) ? args.pop : {}
|
|
403
|
+
opt = opts.delete(key)
|
|
404
|
+
args << opts unless opts.empty?
|
|
405
|
+
opt
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def add_fields!(fields)
|
|
409
|
+
raise 'cannot use both add_fields and include_models' if @included_models
|
|
410
|
+
( @add_fields ||= {} ).merge!(fields)
|
|
411
|
+
|
|
412
|
+
# We have to reload the models because we are adding additional fields.
|
|
413
|
+
self.clear_cache!
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def include_models!(*models)
|
|
417
|
+
raise 'cannot use both add_fields and include_models' if @add_fields
|
|
418
|
+
|
|
419
|
+
# included models to pass to find call (see ActiveResource::Base.find)
|
|
420
|
+
( @included_models ||= [] ).concat(models)
|
|
421
|
+
|
|
422
|
+
# We have to reload the models because we are adding additional fields.
|
|
423
|
+
self.clear_cache!
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def aggregate(*args)
|
|
427
|
+
anchor!(:sql)
|
|
428
|
+
query.aggregate(*args)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def clear_cache!
|
|
432
|
+
@models_by_id = nil
|
|
433
|
+
self
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def merge_cache!(other)
|
|
437
|
+
other_cache = other.models_by_id
|
|
438
|
+
models_by_id.merge!(other_cache)
|
|
439
|
+
self
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def sync
|
|
443
|
+
ids
|
|
444
|
+
self
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def sync_models
|
|
448
|
+
if size <= MAX_CACHE_SIZE
|
|
449
|
+
fetch_models(model_ids)
|
|
450
|
+
end
|
|
451
|
+
self
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def clone_fields
|
|
455
|
+
# Do a deep copy of the fields we want to modify.
|
|
456
|
+
@query = @query.clone if @query
|
|
457
|
+
@add_fields = @add_fields.clone if @add_fields
|
|
458
|
+
@included_models = @included_models.clone if @included_models
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def self.as_set(models)
|
|
462
|
+
models.kind_of?(self) ? models : new(models)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def self.as_ids(models)
|
|
466
|
+
return [] unless models
|
|
467
|
+
if models.kind_of?(self)
|
|
468
|
+
models.ids
|
|
469
|
+
else
|
|
470
|
+
models = [models] if not models.kind_of?(Enumerable)
|
|
471
|
+
models.collect {|model| model.kind_of?(ActiveRecord::Base) ? model.id : model.to_i }
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def self.empty
|
|
476
|
+
new([])
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def self.all
|
|
480
|
+
new(nil)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def self.find(opts)
|
|
484
|
+
set = all
|
|
485
|
+
set.add_joins!(opts[:joins]) if opts[:joins]
|
|
486
|
+
set.add_conditions!(opts[:conditions]) if opts[:conditions]
|
|
487
|
+
set.order_by!(opts[:order]) if opts[:order]
|
|
488
|
+
set.limit!(opts[:limit], opts[:offset]) if opts[:limit]
|
|
489
|
+
set.page!(opts[:page]) if opts[:page]
|
|
490
|
+
set
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def self.find_by_sql(sql)
|
|
494
|
+
query = RawSQLQuery.new(self)
|
|
495
|
+
query.sql = sql
|
|
496
|
+
new(query)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def self.constructor(filter_name, opts = nil)
|
|
500
|
+
(class << self; self; end).module_eval do
|
|
501
|
+
define_method filter_name do |*args|
|
|
502
|
+
if opts
|
|
503
|
+
args.last.kind_of?(Hash) ? args.last.reverse_merge!(opts.clone) : args << opts.clone
|
|
504
|
+
end
|
|
505
|
+
self.all.send("#{filter_name}!", *args)
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# By default the model class is the set class without the trailing "Set".
|
|
511
|
+
# If you use a different model class you can call "model_class MyModel" in your set class.
|
|
512
|
+
def self.model_class(model_class = nil)
|
|
513
|
+
return ActiveRecord::Base if self == ModelSet
|
|
514
|
+
|
|
515
|
+
if model_class.nil?
|
|
516
|
+
@model_class ||= self.name.sub(/#{set_class_suffix}$/,'').constantize
|
|
517
|
+
else
|
|
518
|
+
@model_class = model_class
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def self.query_model_class(query_model_class = nil)
|
|
523
|
+
if query_model_class.nil?
|
|
524
|
+
@query_model_class ||= model_class
|
|
525
|
+
else
|
|
526
|
+
@query_model_class = query_model_class
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def self.model_name
|
|
531
|
+
model_class.name
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def self.set_class_suffix
|
|
535
|
+
'Set'
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def self.table_name(table_name = nil)
|
|
539
|
+
if table_name.nil?
|
|
540
|
+
@table_name ||= model_class.table_name
|
|
541
|
+
else
|
|
542
|
+
@table_name = table_name
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def self.id_field(id_field = nil)
|
|
547
|
+
if id_field.nil?
|
|
548
|
+
@id_field ||= 'id'
|
|
549
|
+
else
|
|
550
|
+
@id_field = id_field
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def self.id_field_with_prefix
|
|
555
|
+
"#{self.table_name}.#{self.id_field}"
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# Define instance methods based on class methods.
|
|
559
|
+
[:model_class, :query_model_class, :model_name, :table_name, :id_field, :id_field_with_prefix].each do |method|
|
|
560
|
+
define_method(method) do |*args|
|
|
561
|
+
self.class.send(method, *args)
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def marshal_dump
|
|
566
|
+
[ @query, @add_fields, @included_models, @created_at ]
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def marshal_load(fields)
|
|
570
|
+
@query, @add_fields, @included_models, @created_at = fields
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
protected
|
|
574
|
+
|
|
575
|
+
def db
|
|
576
|
+
model_class.connection
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def models_by_id
|
|
580
|
+
@models_by_id ||= {}
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def model_ids
|
|
584
|
+
query.ids
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
private
|
|
588
|
+
|
|
589
|
+
def fetch_models(ids_to_fetch)
|
|
590
|
+
ids_to_fetch = ids_to_fetch - models_by_id.keys
|
|
591
|
+
|
|
592
|
+
if not ids_to_fetch.empty?
|
|
593
|
+
if @add_fields.nil? and @included_models.nil?
|
|
594
|
+
models = model_class.send("find_all_by_#{id_field}", ids_to_fetch.to_a)
|
|
595
|
+
else
|
|
596
|
+
fields = ["#{table_name}.*"]
|
|
597
|
+
joins = []
|
|
598
|
+
@add_fields and @add_fields.each do |field, join|
|
|
599
|
+
fields << field
|
|
600
|
+
joins << join
|
|
601
|
+
end
|
|
602
|
+
joins.uniq!
|
|
603
|
+
|
|
604
|
+
models = model_class.find(:all,
|
|
605
|
+
:select => fields.compact.join(','),
|
|
606
|
+
:joins => joins.compact.join(' '),
|
|
607
|
+
:conditions => db.ids_clause(ids_to_fetch, id_field_with_prefix),
|
|
608
|
+
:include => @included_models
|
|
609
|
+
)
|
|
610
|
+
end
|
|
611
|
+
models.each do |model|
|
|
612
|
+
id = model.send(id_field)
|
|
613
|
+
models_by_id[id] ||= model
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def as_id(model)
|
|
619
|
+
case model
|
|
620
|
+
when model_class
|
|
621
|
+
# Save the model object if it is of the same type as our models.
|
|
622
|
+
id = model.send(id_field)
|
|
623
|
+
models_by_id[id] ||= model
|
|
624
|
+
when ActiveRecord::Base
|
|
625
|
+
id = model.id
|
|
626
|
+
else
|
|
627
|
+
id = model.to_i
|
|
628
|
+
end
|
|
629
|
+
raise "id not found for model: #{model.inspect}" if id.nil?
|
|
630
|
+
id
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def as_ids(models)
|
|
634
|
+
return [] unless models
|
|
635
|
+
case models
|
|
636
|
+
when ModelSet
|
|
637
|
+
merge_cache!(models)
|
|
638
|
+
models.ids
|
|
639
|
+
when MultiSet
|
|
640
|
+
models.ids_by_class[model_class]
|
|
641
|
+
else
|
|
642
|
+
models = [models] if not models.kind_of?(Enumerable)
|
|
643
|
+
models.collect {|model| as_id(model) }
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
class ActiveRecord::Base
|
|
649
|
+
def self.has_set(name, options = {}, &extension)
|
|
650
|
+
namespace = self.name.split('::')
|
|
651
|
+
if namespace.empty?
|
|
652
|
+
namespace = ''
|
|
653
|
+
else
|
|
654
|
+
namespace[-1] = ''
|
|
655
|
+
namespace = namespace.join('::')
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
if options[:set_class]
|
|
659
|
+
options[:set_class] = namespace + options[:set_class]
|
|
660
|
+
other_class = options[:set_class].constantize.model_class
|
|
661
|
+
else
|
|
662
|
+
options[:class_name] ||= name.to_s.singularize.camelize
|
|
663
|
+
options[:class_name] = namespace + options[:class_name].to_s
|
|
664
|
+
options[:set_class] = options[:class_name] + 'Set'
|
|
665
|
+
other_class = options[:class_name].constantize
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
set_class = begin
|
|
669
|
+
options[:set_class].constantize
|
|
670
|
+
rescue NameError
|
|
671
|
+
module_eval "class ::#{options[:set_class]} < ModelSet; end"
|
|
672
|
+
options[:set_class].constantize
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
extension_module = if extension
|
|
676
|
+
Module.new(&extension)
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
initial_set_all = if options[:filters] and options[:filters].first == :all
|
|
680
|
+
options[:filters].shift
|
|
681
|
+
true
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
define_method name do |*args|
|
|
685
|
+
@model_set_cache ||= {}
|
|
686
|
+
@model_set_cache[name] = nil if args.first == true # Reload the set.
|
|
687
|
+
if @model_set_cache[name].nil?
|
|
688
|
+
|
|
689
|
+
if initial_set_all
|
|
690
|
+
set = set_class.all
|
|
691
|
+
else
|
|
692
|
+
own_key = options[:own_key] || self.class.table_name.singularize + '_id'
|
|
693
|
+
if options[:as]
|
|
694
|
+
as_clause = "AND #{options[:as]}_type = '#{self.class}'"
|
|
695
|
+
own_key = "#{options[:as]}_id" unless options[:own_key]
|
|
696
|
+
end
|
|
697
|
+
if options[:through]
|
|
698
|
+
other_key = options[:other_key] || other_class.table_name.singularize + '_id'
|
|
699
|
+
where_clause = "#{own_key} = #{id}"
|
|
700
|
+
where_clause << " AND #{options[:through_conditions]}" if options[:through_conditions]
|
|
701
|
+
set = set_class.find_by_sql %{
|
|
702
|
+
SELECT #{other_key} FROM #{options[:through]}
|
|
703
|
+
WHERE #{where_clause} #{as_clause}
|
|
704
|
+
}
|
|
705
|
+
else
|
|
706
|
+
set = set_class.all.add_conditions!("#{set_class.table_name}.#{own_key} = #{id} #{as_clause}")
|
|
707
|
+
end
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
set.instance_variable_set(:@parent_model, self)
|
|
711
|
+
def set.parent_model
|
|
712
|
+
@parent_model
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
if options[:filters]
|
|
716
|
+
options[:filters].each do |filter_name|
|
|
717
|
+
filter_name = "#{filter_name}!"
|
|
718
|
+
if set.method(filter_name).arity == 0
|
|
719
|
+
set.send(filter_name)
|
|
720
|
+
else
|
|
721
|
+
set.send(filter_name, self)
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
set.add_joins!(options[:joins]) if options[:joins]
|
|
727
|
+
set.add_conditions!(options[:conditions]) if options[:conditions]
|
|
728
|
+
set.order_by!(options[:order]) if options[:order]
|
|
729
|
+
set.extend(extension_module) if extension_module
|
|
730
|
+
@model_set_cache[name] = set
|
|
731
|
+
end
|
|
732
|
+
if options[:clone] == false or args.include?(:no_clone)
|
|
733
|
+
@model_set_cache[name]
|
|
734
|
+
else
|
|
735
|
+
@model_set_cache[name].clone
|
|
736
|
+
end
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
define_method :reset_model_set_cache do
|
|
740
|
+
@model_set_cache = {}
|
|
741
|
+
end
|
|
742
|
+
end
|
|
743
|
+
end
|