ninjudd-model_set 0.9.2

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