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.
- data/LICENSE +20 -0
- data/README.rdoc +39 -0
- data/VERSION.yml +4 -0
- data/lib/model_set.rb +712 -0
- data/lib/model_set/conditioned.rb +33 -0
- data/lib/model_set/conditions.rb +103 -0
- data/lib/model_set/query.rb +128 -0
- data/lib/model_set/raw_query.rb +41 -0
- data/lib/model_set/raw_sql_query.rb +19 -0
- data/lib/model_set/set_query.rb +34 -0
- data/lib/model_set/solr_query.rb +70 -0
- data/lib/model_set/sphinx_query.rb +148 -0
- data/lib/model_set/sql_base_query.rb +52 -0
- data/lib/model_set/sql_query.rb +75 -0
- data/lib/multi_set.rb +67 -0
- data/test/model_set_test.rb +283 -0
- data/test/multi_set_test.rb +65 -0
- data/test/test_helper.rb +23 -0
- data/vendor/sphinx_client/README.rdoc +41 -0
- data/vendor/sphinx_client/Rakefile +21 -0
- data/vendor/sphinx_client/init.rb +1 -0
- data/vendor/sphinx_client/install.rb +5 -0
- data/vendor/sphinx_client/lib/sphinx.rb +6 -0
- data/vendor/sphinx_client/lib/sphinx/client.rb +1093 -0
- data/vendor/sphinx_client/lib/sphinx/request.rb +50 -0
- data/vendor/sphinx_client/lib/sphinx/response.rb +69 -0
- data/vendor/sphinx_client/spec/client_response_spec.rb +112 -0
- data/vendor/sphinx_client/spec/client_spec.rb +469 -0
- data/vendor/sphinx_client/spec/fixtures/default_search.php +8 -0
- data/vendor/sphinx_client/spec/fixtures/default_search_index.php +8 -0
- data/vendor/sphinx_client/spec/fixtures/excerpt_custom.php +11 -0
- data/vendor/sphinx_client/spec/fixtures/excerpt_default.php +8 -0
- data/vendor/sphinx_client/spec/fixtures/excerpt_flags.php +11 -0
- data/vendor/sphinx_client/spec/fixtures/field_weights.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter_exclude.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter_float_range.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter_float_range_exclude.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter_range.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter_range_exclude.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter_range_int64.php +10 -0
- data/vendor/sphinx_client/spec/fixtures/filter_ranges.php +10 -0
- data/vendor/sphinx_client/spec/fixtures/filters.php +10 -0
- data/vendor/sphinx_client/spec/fixtures/filters_different.php +13 -0
- data/vendor/sphinx_client/spec/fixtures/geo_anchor.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_attr.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_attrpair.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_day.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_day_sort.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_month.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_week.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_year.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_distinct.php +10 -0
- data/vendor/sphinx_client/spec/fixtures/id_range.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/id_range64.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/index_weights.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/keywords.php +8 -0
- data/vendor/sphinx_client/spec/fixtures/limits.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/limits_cutoff.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/limits_max.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/limits_max_cutoff.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_all.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_any.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_boolean.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_extended.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_extended2.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_fullscan.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_phrase.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/max_query_time.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/miltiple_queries.php +12 -0
- data/vendor/sphinx_client/spec/fixtures/ranking_bm25.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/ranking_none.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/ranking_proximity.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/ranking_proximity_bm25.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/ranking_wordcount.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/retries.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/retries_delay.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/select.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/set_override.php +11 -0
- data/vendor/sphinx_client/spec/fixtures/sort_attr_asc.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/sort_attr_desc.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/sort_expr.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/sort_extended.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/sort_relevance.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/sort_time_segments.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/sphinxapi.php +1269 -0
- data/vendor/sphinx_client/spec/fixtures/update_attributes.php +8 -0
- data/vendor/sphinx_client/spec/fixtures/update_attributes_mva.php +8 -0
- data/vendor/sphinx_client/spec/fixtures/weights.php +9 -0
- data/vendor/sphinx_client/spec/sphinx/sphinx-id64.conf +67 -0
- data/vendor/sphinx_client/spec/sphinx/sphinx.conf +67 -0
- data/vendor/sphinx_client/spec/sphinx/sphinx_test.sql +86 -0
- data/vendor/sphinx_client/sphinx.yml.tpl +3 -0
- data/vendor/sphinx_client/tasks/sphinx.rake +75 -0
- metadata +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
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
|