ninjudd-model_set 0.9.2

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