model_set 0.10.6

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 +5 -0
  4. data/lib/model_set/conditioned.rb +33 -0
  5. data/lib/model_set/conditions.rb +103 -0
  6. data/lib/model_set/query.rb +132 -0
  7. data/lib/model_set/raw_query.rb +41 -0
  8. data/lib/model_set/raw_sql_query.rb +19 -0
  9. data/lib/model_set/set_query.rb +34 -0
  10. data/lib/model_set/solr_query.rb +70 -0
  11. data/lib/model_set/sphinx_query.rb +206 -0
  12. data/lib/model_set/sql_base_query.rb +52 -0
  13. data/lib/model_set/sql_query.rb +109 -0
  14. data/lib/model_set.rb +743 -0
  15. data/lib/multi_set.rb +67 -0
  16. data/test/model_set_test.rb +329 -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/client.rb +1093 -0
  24. data/vendor/sphinx_client/lib/sphinx/request.rb +50 -0
  25. data/vendor/sphinx_client/lib/sphinx/response.rb +69 -0
  26. data/vendor/sphinx_client/lib/sphinx.rb +6 -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 +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