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.
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