mochigome 0.2.2 → 0.2.3
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/lib/mochigome_ver.rb +1 -1
- data/lib/model_extensions.rb +5 -0
- data/lib/model_graph.rb +70 -12
- data/lib/relation.rb +8 -8
- data/test/app_root/app/models/favorite.rb +6 -0
- data/test/app_root/app/models/owner.rb +2 -0
- data/test/app_root/app/models/product.rb +1 -0
- data/test/app_root/db/migrate/20110817163830_create_tables.rb +6 -0
- data/test/factories.rb +5 -0
- data/test/unit/query_test.rb +20 -1
- metadata +5 -4
data/lib/mochigome_ver.rb
CHANGED
data/lib/model_extensions.rb
CHANGED
@@ -189,6 +189,7 @@ module Mochigome
|
|
189
189
|
@options[:custom_subgroup_exprs] = ActiveSupport::OrderedHash.new
|
190
190
|
@options[:custom_assocs] = ActiveSupport::OrderedHash.new
|
191
191
|
@options[:ignore_assocs] = Set.new
|
192
|
+
@options[:preferred_paths] = {}
|
192
193
|
end
|
193
194
|
|
194
195
|
def type_name(n)
|
@@ -247,6 +248,10 @@ module Mochigome
|
|
247
248
|
def ignore_assoc(name)
|
248
249
|
@options[:ignore_assocs].add name.to_sym
|
249
250
|
end
|
251
|
+
|
252
|
+
def preferred_path_to(target_model, assoc_name)
|
253
|
+
@options[:preferred_paths][target_model.name] = assoc_name
|
254
|
+
end
|
250
255
|
end
|
251
256
|
|
252
257
|
class AggregationSettings
|
data/lib/model_graph.rb
CHANGED
@@ -120,7 +120,8 @@ module Mochigome
|
|
120
120
|
|
121
121
|
ignore_assocs = []
|
122
122
|
if model.acts_as_mochigome_focus?
|
123
|
-
|
123
|
+
opts = model.mochigome_focus_settings.options
|
124
|
+
ignore_assocs = opts[:ignore_assocs]
|
124
125
|
end
|
125
126
|
|
126
127
|
model.reflections.
|
@@ -157,12 +158,6 @@ module Mochigome
|
|
157
158
|
end
|
158
159
|
|
159
160
|
added_models.each do |model|
|
160
|
-
# FIXME: Un-DRY, this is a C&P from above
|
161
|
-
ignore_assocs = []
|
162
|
-
if model.acts_as_mochigome_focus?
|
163
|
-
ignore_assocs = model.mochigome_focus_settings.options[:ignore_assocs]
|
164
|
-
end
|
165
|
-
|
166
161
|
next unless @assoc_graph.has_vertex?(model)
|
167
162
|
path_tree = @assoc_graph.bfs_search_tree_from(model).reverse
|
168
163
|
path_tree.depth_first_search do |tgt_model|
|
@@ -173,10 +168,22 @@ module Mochigome
|
|
173
168
|
end
|
174
169
|
@shortest_paths[[model,tgt_model]] = path
|
175
170
|
end
|
171
|
+
end
|
172
|
+
|
173
|
+
added_models.each do |model|
|
174
|
+
ignore_assocs, model_preferred_paths = [], {}
|
175
|
+
if model.acts_as_mochigome_focus?
|
176
|
+
opts = model.mochigome_focus_settings.options
|
177
|
+
ignore_assocs = opts[:ignore_assocs]
|
178
|
+
model_preferred_paths = opts[:preferred_paths]
|
179
|
+
end
|
180
|
+
|
181
|
+
preferred_paths = {}
|
176
182
|
|
177
183
|
# Use through reflections as a hint for preferred indirect paths
|
178
|
-
|
179
|
-
|
184
|
+
# TODO Support for nested through reflections
|
185
|
+
# (Though Rails 2 doesn't support them either...)
|
186
|
+
model.reflections.select{|name, assoc| assoc.through_reflection}.
|
180
187
|
reject{|name, assoc| ignore_assocs.include? name.to_sym}.
|
181
188
|
reject{|name, assoc| ignore_assocs.include? assoc.through_reflection.name.to_sym}.
|
182
189
|
to_a.sort{|a,b| a.first.to_s <=> b.first.to_s}.
|
@@ -187,9 +194,60 @@ module Mochigome
|
|
187
194
|
rescue NameError
|
188
195
|
# FIXME Can't handle polymorphic through reflection
|
189
196
|
end
|
190
|
-
edge = [model,foreign_model]
|
191
|
-
|
192
|
-
@shortest_paths[edge]
|
197
|
+
edge = [model, foreign_model]
|
198
|
+
path = [model, join_model, foreign_model]
|
199
|
+
next if @shortest_paths[edge].try(:size).try(:<, path.size)
|
200
|
+
preferred_paths[edge] = path
|
201
|
+
end
|
202
|
+
|
203
|
+
# Model focus can specify paths with prefered_path_to
|
204
|
+
model_preferred_paths.each do |tgt_model_name, assoc_name|
|
205
|
+
tgt_model = tgt_model_name.constantize
|
206
|
+
edge = [model, tgt_model]
|
207
|
+
assoc = model.reflections[assoc_name]
|
208
|
+
sub_path = @shortest_paths[[assoc.klass, tgt_model]]
|
209
|
+
unless sub_path
|
210
|
+
raise ModelSetupError.new(
|
211
|
+
"Can't find subpath to #{tgt_model} via #{model.name}.#{assoc_name}"
|
212
|
+
)
|
213
|
+
end
|
214
|
+
if sub_path.include?(model)
|
215
|
+
raise ModelSetupError.new(
|
216
|
+
"Subpath to #{tgt_model} via #{model.name}.#{assoc_name} loops back"
|
217
|
+
)
|
218
|
+
end
|
219
|
+
sub_link = @shortest_paths[[model, assoc.klass]]
|
220
|
+
preferred_paths[edge] = sub_link + sub_path.drop(1)
|
221
|
+
end
|
222
|
+
|
223
|
+
# Replace all instances of the default path in the path directory
|
224
|
+
# with the preferred path, including when the default path is
|
225
|
+
# a subset of a larger path, and/or when the direction of travel
|
226
|
+
# is reversed.
|
227
|
+
# FIXME What if preferred paths conflict?
|
228
|
+
# FIXME What if one preferred path causes a shortest_path to become
|
229
|
+
# applicable under another one? Then arbitrary model scanning
|
230
|
+
# order matters, and it shouldn't. Is there even a consistent
|
231
|
+
# way to deal with this?
|
232
|
+
preferred_paths.each do |edge, path|
|
233
|
+
[lambda{|a| a}, lambda{|a| a.reverse}].each do |prc|
|
234
|
+
e, p = prc.call(edge), prc.call(path)
|
235
|
+
old_path = @shortest_paths[e]
|
236
|
+
next if old_path == p
|
237
|
+
edges_to_replace = {}
|
238
|
+
@shortest_paths.each do |se, sp|
|
239
|
+
p_begin = sp.find_index(old_path.first)
|
240
|
+
if p_begin && sp[p_begin, old_path.size] == old_path
|
241
|
+
edges_to_replace[se] =
|
242
|
+
sp.take(p_begin) +
|
243
|
+
p +
|
244
|
+
sp.drop(p_begin + old_path.size)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
edges_to_replace.each do |re, rp|
|
248
|
+
@shortest_paths[re] = rp
|
249
|
+
end
|
250
|
+
end
|
193
251
|
end
|
194
252
|
end
|
195
253
|
end
|
data/lib/relation.rb
CHANGED
@@ -51,8 +51,8 @@ module Mochigome
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
-
raise QueryError.new("No path to #{model}") unless best_path
|
55
|
-
join_on_path
|
54
|
+
raise QueryError.new("No path to #{model} from #{@models.map(&:name).inspect}") unless best_path
|
55
|
+
join_on_path best_path, "Best path to model #{model}"
|
56
56
|
|
57
57
|
# Also use the conditions of any other unique path
|
58
58
|
# TODO: Write a test that requires the below code to work
|
@@ -60,22 +60,22 @@ module Mochigome
|
|
60
60
|
extra_path = @model_graph.path_thru([n, model])
|
61
61
|
if extra_path
|
62
62
|
unless best_path.all?{|m| extra_path.include?(m)}
|
63
|
-
join_on_path extra_path
|
63
|
+
join_on_path extra_path, "Additional path to model #{model}"
|
64
64
|
end
|
65
65
|
end
|
66
66
|
end
|
67
67
|
end
|
68
68
|
|
69
|
-
def join_on_path_thru(path)
|
69
|
+
def join_on_path_thru(path, descrip = nil)
|
70
70
|
full_path = @model_graph.path_thru(path)
|
71
71
|
if full_path
|
72
|
-
join_on_path(full_path)
|
72
|
+
join_on_path(full_path, descrip || "Generic path thru #{path.map(&:name).inspect}")
|
73
73
|
else
|
74
74
|
raise QueryError.new("Cannot route thru #{path.map(&:name).inspect}")
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
78
|
-
def join_on_path(path,
|
78
|
+
def join_on_path(path, descrip = "Generic")
|
79
79
|
begin
|
80
80
|
path = path.map(&:to_real_model).uniq
|
81
81
|
join_to_model path.first
|
@@ -89,7 +89,7 @@ module Mochigome
|
|
89
89
|
join_descrips << tgt.name
|
90
90
|
end
|
91
91
|
end
|
92
|
-
@join_path_descriptions << join_descrips.join("->")
|
92
|
+
@join_path_descriptions << "#{join_descrips.join("->")} (#{descrip})"
|
93
93
|
rescue QueryError => e
|
94
94
|
raise QueryError.new("Error pathing #{path.map(&:name).inspect}: #{e}")
|
95
95
|
end
|
@@ -130,7 +130,7 @@ module Mochigome
|
|
130
130
|
# FIXME: Eventually we need to support joins that
|
131
131
|
# double back, if only for CanCan stuff, so get rid of this
|
132
132
|
# uniq junk.
|
133
|
-
join_on_path_thru path.uniq
|
133
|
+
join_on_path_thru path.uniq, "Access filter for #{m.name}"
|
134
134
|
end
|
135
135
|
if h[:condition]
|
136
136
|
apply_condition h.delete(:condition)
|
@@ -38,6 +38,12 @@ class CreateTables < ActiveRecord::Migration
|
|
38
38
|
t.timestamps
|
39
39
|
end
|
40
40
|
|
41
|
+
# Used to create an alternate path to try and confuse ModelGraph
|
42
|
+
create_table :favorites do |t|
|
43
|
+
t.integer :owner_id
|
44
|
+
t.integer :product_id
|
45
|
+
end
|
46
|
+
|
41
47
|
# Used by ModelExtensionTest to create temporary models
|
42
48
|
create_table :fake do |t|
|
43
49
|
t.timestamps
|
data/test/factories.rb
CHANGED
data/test/unit/query_test.rb
CHANGED
@@ -41,6 +41,11 @@ describe Mochigome::Query do
|
|
41
41
|
n.times{create(:sale, :store_product => sp)}
|
42
42
|
end
|
43
43
|
|
44
|
+
create(:favorite, :owner => @john, :product => @product_c)
|
45
|
+
create(:favorite, :owner => @john, :product => @product_d)
|
46
|
+
create(:favorite, :owner => @jane, :product => @product_c)
|
47
|
+
create(:favorite, :owner => @jane, :product => @product_a)
|
48
|
+
|
44
49
|
(1..5).each do |div_num|
|
45
50
|
WidgetDivisor.create(:divisor => div_num)
|
46
51
|
end
|
@@ -318,6 +323,20 @@ describe Mochigome::Query do
|
|
318
323
|
assert_empty data_node.children
|
319
324
|
end
|
320
325
|
|
326
|
+
it "can collect aggregate data in non-association-order layer designs" do
|
327
|
+
q = Mochigome::Query.new([Product, Owner, Store], :aggregate_sources => [Sale])
|
328
|
+
data_node = q.run
|
329
|
+
assert_equal_children [
|
330
|
+
@product_a, @product_b, @product_c, @product_d, @product_e
|
331
|
+
], data_node
|
332
|
+
assert_equal_children [@john, @jane], data_node/0
|
333
|
+
assert_equal_children [@jane], data_node/1
|
334
|
+
assert_equal_children [@store_x], data_node/0/0
|
335
|
+
assert_equal_children [@store_y], data_node/1/0
|
336
|
+
assert_equal 9, (data_node/0)["Sales count"]
|
337
|
+
assert_equal 5, (data_node/0/0)["Sales count"]
|
338
|
+
end
|
339
|
+
|
321
340
|
it "can use a named aggregate data setting" do
|
322
341
|
q = Mochigome::Query.new(
|
323
342
|
[Owner],
|
@@ -531,7 +550,7 @@ describe Mochigome::Query do
|
|
531
550
|
|
532
551
|
# TODO: Test use of non-trivial function for aggregation value
|
533
552
|
|
534
|
-
it "
|
553
|
+
it "sets a comment on the root node describing the query" do
|
535
554
|
q = Mochigome::Query.new([Owner, Store, Product])
|
536
555
|
data_node = q.run([@store_x, @store_y, @store_z])
|
537
556
|
c = data_node.comment
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mochigome
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 17
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 2
|
9
|
-
-
|
10
|
-
version: 0.2.
|
9
|
+
- 3
|
10
|
+
version: 0.2.3
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- David Mike Simon
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2012-10-
|
18
|
+
date: 2012-10-25 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
version_requirements: &id001 !ruby/object:Gem::Requirement
|
@@ -121,6 +121,7 @@ files:
|
|
121
121
|
- test/app_root/app/helpers/application_helper.rb
|
122
122
|
- test/app_root/app/models/boring_datum.rb
|
123
123
|
- test/app_root/app/models/category.rb
|
124
|
+
- test/app_root/app/models/favorite.rb
|
124
125
|
- test/app_root/app/models/owner.rb
|
125
126
|
- test/app_root/app/models/product.rb
|
126
127
|
- test/app_root/app/models/sale.rb
|