mochigome 0.0.8 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -1
- data/Gemfile.lock +2 -2
- data/lib/arel_rails2_hacks.rb +1 -1
- data/lib/data_node.rb +6 -0
- data/lib/mochigome.rb +1 -0
- data/lib/mochigome_ver.rb +1 -1
- data/lib/model_extensions.rb +13 -0
- data/lib/query.rb +77 -49
- data/lib/subgroup_model.rb +102 -0
- data/test/app_root/config/database-my.yml +2 -2
- data/test/unit/data_node_test.rb +9 -0
- data/test/unit/query_test.rb +79 -7
- metadata +5 -4
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -23,7 +23,7 @@ GEM
|
|
23
23
|
minitest (2.5.0)
|
24
24
|
mynyml-redgreen (0.7.1)
|
25
25
|
term-ansicolor (>= 1.0.4)
|
26
|
-
|
26
|
+
mysql2 (0.2.18)
|
27
27
|
nokogiri (1.5.0)
|
28
28
|
pdf-writer (1.1.8)
|
29
29
|
color (>= 1.4.0)
|
@@ -64,7 +64,7 @@ DEPENDENCIES
|
|
64
64
|
factory_girl (= 2.0.4)
|
65
65
|
minitest
|
66
66
|
mynyml-redgreen
|
67
|
-
|
67
|
+
mysql2 (~> 0.2.0)
|
68
68
|
nokogiri
|
69
69
|
rails (= 2.3.12)
|
70
70
|
rcov
|
data/lib/arel_rails2_hacks.rb
CHANGED
@@ -42,7 +42,7 @@ unless ActiveRecord::ConnectionAdapters::ConnectionPool.methods.include?("table_
|
|
42
42
|
class ActiveRecord::ConnectionAdapters::SQLiteAdapter
|
43
43
|
def select_rows(sql, name = nil)
|
44
44
|
execute(sql, name).map do |row|
|
45
|
-
row.keys.select{|key| key.is_a? Integer}.map{|key| row[key]}
|
45
|
+
row.keys.select{|key| key.is_a? Integer}.sort.map{|key| row[key]}
|
46
46
|
end
|
47
47
|
end
|
48
48
|
end
|
data/lib/data_node.rb
CHANGED
@@ -41,6 +41,12 @@ module Mochigome
|
|
41
41
|
@children[idx]
|
42
42
|
end
|
43
43
|
|
44
|
+
def dup
|
45
|
+
twin = super
|
46
|
+
twin.instance_variable_set(:@children, @children.map{|c| c.dup})
|
47
|
+
twin
|
48
|
+
end
|
49
|
+
|
44
50
|
# TODO: Only define xml-related methods if nokogiri loaded
|
45
51
|
def to_xml
|
46
52
|
doc = Nokogiri::XML::Document.new
|
data/lib/mochigome.rb
CHANGED
data/lib/mochigome_ver.rb
CHANGED
data/lib/model_extensions.rb
CHANGED
@@ -12,6 +12,19 @@ module Mochigome
|
|
12
12
|
end
|
13
13
|
|
14
14
|
module ClassMethods
|
15
|
+
def real_model?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
# TODO: Use this instead of calling Table.new all over the place
|
20
|
+
def arel_table
|
21
|
+
Arel::Table.new(table_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def arel_primary_key
|
25
|
+
arel_table[primary_key]
|
26
|
+
end
|
27
|
+
|
15
28
|
def acts_as_mochigome_focus
|
16
29
|
if self.try(:mochigome_focus_settings).try(:model) == self
|
17
30
|
raise Mochigome::ModelSetupError.new("Already acts_as_mochigome_focus for #{self.name}")
|
data/lib/query.rb
CHANGED
@@ -4,9 +4,10 @@ require 'rgl/traversal'
|
|
4
4
|
module Mochigome
|
5
5
|
class Query
|
6
6
|
def initialize(layer_types, options = {})
|
7
|
-
# TODO: Validate layer types: not empty, AR, act_as_mochigome_focus
|
7
|
+
# TODO: Validate layer types: not empty, AR, act_as_mochigome_focus
|
8
8
|
@layer_types = layer_types
|
9
|
-
@layers_path = self.class.path_thru(layer_types)
|
9
|
+
@layers_path = self.class.path_thru(@layer_types)
|
10
|
+
@layers_path or raise QueryError.new("No valid path thru layer list") #TODO Test
|
10
11
|
|
11
12
|
@name = options.delete(:root_name).try(:to_s) || "report"
|
12
13
|
@access_filter = options.delete(:access_filter) || lambda {|cls| {}}
|
@@ -16,14 +17,17 @@ module Mochigome
|
|
16
17
|
end
|
17
18
|
|
18
19
|
@ids_rel = self.class.relation_over_path(@layers_path).
|
19
|
-
project(@layers_path.map{|m|
|
20
|
-
Arel::Table.new(m.table_name)[m.primary_key]
|
21
|
-
})
|
20
|
+
project(@layers_path.map{|m| m.arel_primary_key})
|
22
21
|
@ids_rel = access_filtered_relation(@ids_rel, @layers_path)
|
23
22
|
|
24
23
|
# TODO: Validate that aggregate_sources is in the correct format
|
25
24
|
aggs_by_model = {}
|
26
|
-
aggregate_sources.each do |
|
25
|
+
aggregate_sources.each do |a|
|
26
|
+
if a.instance_of?(Array)
|
27
|
+
focus_cls, data_cls = a.first, a.second
|
28
|
+
else
|
29
|
+
focus_cls, data_cls = a, a
|
30
|
+
end
|
27
31
|
aggs_by_model[focus_cls] ||= []
|
28
32
|
aggs_by_model[focus_cls] << data_cls
|
29
33
|
end
|
@@ -39,7 +43,12 @@ module Mochigome
|
|
39
43
|
|
40
44
|
@aggregate_rels[focus_model] = {}
|
41
45
|
data_models.each do |data_model|
|
42
|
-
|
46
|
+
if focus_model == data_model
|
47
|
+
f2d_path = [focus_model]
|
48
|
+
else
|
49
|
+
#TODO: Handle nil here
|
50
|
+
f2d_path = self.class.path_thru([focus_model, data_model])
|
51
|
+
end
|
43
52
|
agg_path = nil
|
44
53
|
key_path = nil
|
45
54
|
f2d_path.each do |link_model|
|
@@ -63,15 +72,12 @@ module Mochigome
|
|
63
72
|
end
|
64
73
|
end
|
65
74
|
|
66
|
-
key_cols = key_path.map{|m|
|
67
|
-
Arel::Table.new(m.table_name)[m.primary_key]
|
68
|
-
}
|
75
|
+
key_cols = key_path.map{|m| m.arel_primary_key }
|
69
76
|
|
70
77
|
agg_data_rel = self.class.relation_over_path(agg_path, focus_rel.dup)
|
71
78
|
agg_data_rel = access_filtered_relation(agg_data_rel, @layers_path + agg_path)
|
72
79
|
data_tbl = Arel::Table.new(data_model.table_name)
|
73
80
|
agg_fields = data_model.mochigome_aggregation_settings.options[:fields].reject{|a| a[:in_ruby]}
|
74
|
-
agg_data_rel.project # FIXME ??? What is this for?
|
75
81
|
agg_fields.each_with_index do |a, i|
|
76
82
|
agg_data_rel.project(a[:value_proc].call(data_tbl).as("d#{i}"))
|
77
83
|
end
|
@@ -115,22 +121,29 @@ module Mochigome
|
|
115
121
|
end
|
116
122
|
if cond.is_a?(Array)
|
117
123
|
return root if cond.empty?
|
118
|
-
|
119
|
-
|
124
|
+
cond = cond.inject(nil) do |expr, obj|
|
125
|
+
cls = obj.class
|
126
|
+
tbl = Arel::Table.new(cls.table_name)
|
127
|
+
subexpr = tbl[cls.primary_key].eq(obj.id)
|
128
|
+
expr ? expr.or(subexpr) : subexpr
|
120
129
|
end
|
121
|
-
|
122
|
-
|
130
|
+
end
|
131
|
+
if cond
|
132
|
+
self.class.expr_tables(cond).each do |t|
|
133
|
+
raise QueryError.new("Condition table #{t} not in layer list") unless
|
134
|
+
@layers_path.any?{|m| m.table_name == t}
|
123
135
|
end
|
124
|
-
cls = cond.first.class
|
125
|
-
cond = Arel::Table.new(cls.table_name)[cls.primary_key].in(cond.map(&:id))
|
126
136
|
end
|
127
137
|
|
128
138
|
q = @ids_rel.dup
|
129
139
|
q.where(cond) if cond
|
130
140
|
ids_table = @layer_types.first.connection.select_rows(q.to_sql)
|
131
|
-
ids_table = ids_table.map
|
141
|
+
ids_table = ids_table.map do |row|
|
142
|
+
# FIXME: Should do this conversion based on type of column
|
143
|
+
row.map{|cell| cell =~ /^\d+$/ ? cell.to_i : cell}
|
144
|
+
end
|
132
145
|
|
133
|
-
fill_layers(ids_table, {
|
146
|
+
fill_layers(ids_table, {[] => root}, @layer_types)
|
134
147
|
|
135
148
|
@aggregate_rels.each do |focus_model, data_model_rels|
|
136
149
|
super_types = @layer_types.take_while{|m| m != focus_model}
|
@@ -150,10 +163,10 @@ module Mochigome
|
|
150
163
|
c = data_tree
|
151
164
|
super_cols.each_with_index do |sc_num, sc_idx|
|
152
165
|
break if aggs_count+sc_idx >= row.size-1
|
153
|
-
col_num = aggs_count +
|
154
|
-
c = (c[row[col_num]
|
166
|
+
col_num = aggs_count + sc_num
|
167
|
+
c = (c[row[col_num]] ||= {})
|
155
168
|
end
|
156
|
-
c[row.last
|
169
|
+
c[row.last] = row.take(aggs_count)
|
157
170
|
end
|
158
171
|
end
|
159
172
|
insert_aggregate_data_fields(root, data_tree, data_model)
|
@@ -196,7 +209,7 @@ module Mochigome
|
|
196
209
|
r
|
197
210
|
end
|
198
211
|
|
199
|
-
def fill_layers(ids_table, parents, types,
|
212
|
+
def fill_layers(ids_table, parents, types, parent_col_nums = [])
|
200
213
|
return if types.size == 0
|
201
214
|
|
202
215
|
model = types.first
|
@@ -207,10 +220,8 @@ module Mochigome
|
|
207
220
|
ids_table.each do |row|
|
208
221
|
cur_id = row[col_num]
|
209
222
|
layer_ids.add cur_id
|
210
|
-
|
211
|
-
|
212
|
-
cur_to_parent[cur_id].add row[parent_col_num]
|
213
|
-
end
|
223
|
+
cur_to_parent[cur_id] ||= Set.new
|
224
|
+
cur_to_parent[cur_id].add parent_col_nums.map{|i| row[i]}
|
214
225
|
end
|
215
226
|
|
216
227
|
layer = {}
|
@@ -221,23 +232,19 @@ module Mochigome
|
|
221
232
|
f = obj.mochigome_focus
|
222
233
|
dn = DataNode.new(f.type_name, f.name)
|
223
234
|
dn.merge!(f.field_data)
|
235
|
+
|
224
236
|
# TODO: Maybe make special fields below part of ModelExtensions?
|
225
237
|
dn[:id] = obj.id
|
226
|
-
dn[:internal_type] =
|
238
|
+
dn[:internal_type] = model.name
|
227
239
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
duping = true
|
233
|
-
end
|
234
|
-
else
|
235
|
-
parents[:root] << dn
|
240
|
+
cur_to_parent.fetch(obj.id).each do |parent_ids_seq|
|
241
|
+
duped = dn.dup
|
242
|
+
parents.fetch(parent_ids_seq) << duped
|
243
|
+
layer[parent_ids_seq + [obj.id]] = duped
|
236
244
|
end
|
237
|
-
layer[obj.id] = dn
|
238
245
|
end
|
239
246
|
|
240
|
-
fill_layers(ids_table, layer, types.drop(1), col_num)
|
247
|
+
fill_layers(ids_table, layer, types.drop(1), parent_col_nums + [col_num])
|
241
248
|
end
|
242
249
|
|
243
250
|
def insert_aggregate_data_fields(node, table, data_model)
|
@@ -263,15 +270,28 @@ module Mochigome
|
|
263
270
|
|
264
271
|
# TODO: Move the stuff below into its own module
|
265
272
|
|
273
|
+
def self.expr_tables(e)
|
274
|
+
# TODO: This is kind of hacky, Arel probably has a better way
|
275
|
+
# to do this with its API.
|
276
|
+
r = Set.new
|
277
|
+
[:expr, :left, :right].each do |m|
|
278
|
+
r += expr_tables(e.send(m)) if e.respond_to?(m)
|
279
|
+
end
|
280
|
+
r.add e.relation.name if e.respond_to?(:relation)
|
281
|
+
r
|
282
|
+
end
|
283
|
+
|
266
284
|
@@graphed_models = Set.new
|
267
285
|
@@assoc_graph = RGL::DirectedAdjacencyGraph.new
|
268
286
|
@@edge_relation_funcs = {}
|
269
287
|
@@shortest_paths = {}
|
270
288
|
|
271
289
|
def self.relation_over_path(path, rel = nil)
|
272
|
-
|
273
|
-
|
274
|
-
|
290
|
+
# Project ensures that we don't return a Table even if path is empty
|
291
|
+
real_path = path.map{|e| (e.real_model? ? e : e.model)}.uniq
|
292
|
+
rel ||= Arel::Table.new(real_path.first.table_name).project
|
293
|
+
(0..(real_path.size-2)).each do |i|
|
294
|
+
rel = relation_func(real_path[i], real_path[i+1]).call(rel)
|
275
295
|
end
|
276
296
|
rel
|
277
297
|
end
|
@@ -283,14 +303,21 @@ module Mochigome
|
|
283
303
|
|
284
304
|
def self.path_thru(models)
|
285
305
|
update_assoc_graph(models)
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
306
|
+
model_queue = models.dup
|
307
|
+
path = [model_queue.shift]
|
308
|
+
until model_queue.empty?
|
309
|
+
src = path.last
|
310
|
+
tgt = model_queue.shift
|
311
|
+
real_src = src.real_model? ? src : src.model
|
312
|
+
real_tgt = tgt.real_model? ? tgt : tgt.model
|
313
|
+
unless real_src == real_tgt
|
314
|
+
seg = @@shortest_paths[[real_src,real_tgt]]
|
315
|
+
unless seg
|
316
|
+
raise QueryError.new("No path: #{real_src.name} to #{real_tgt.name}")
|
317
|
+
end
|
318
|
+
path.concat seg.take(seg.size-1).drop(1)
|
319
|
+
end
|
320
|
+
path << tgt
|
294
321
|
end
|
295
322
|
unless path.uniq.size == path.size
|
296
323
|
raise QueryError.new(
|
@@ -306,6 +333,7 @@ module Mochigome
|
|
306
333
|
added_models = []
|
307
334
|
until model_queue.empty?
|
308
335
|
model = model_queue.shift
|
336
|
+
next if model.is_a?(SubgroupModel)
|
309
337
|
next if @@graphed_models.include? model
|
310
338
|
@@graphed_models.add model
|
311
339
|
added_models << model
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Mochigome
|
2
|
+
# An instance of SubgroupModel acts like a class that derives from
|
3
|
+
# AR::Base, but is used to do subgrouping in Query and does not
|
4
|
+
# interact with the database itself.
|
5
|
+
class SubgroupModel
|
6
|
+
attr_reader :model, :attr
|
7
|
+
|
8
|
+
def initialize(model, attr)
|
9
|
+
@model = model
|
10
|
+
@attr = attr
|
11
|
+
@focus_settings = Mochigome::ReportFocusSettings.new(@model)
|
12
|
+
@focus_settings.type_name "#{@model.human_name} #{@attr.to_s.humanize}"
|
13
|
+
@focus_settings.name lambda{|r| r.send(attr)}
|
14
|
+
end
|
15
|
+
|
16
|
+
def name
|
17
|
+
"#{@model}%#{@attr}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def human_name
|
21
|
+
"#{@model.human_name} #{@attr.to_s.humanize}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def real_model?
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
def arel_table
|
29
|
+
@model.arel_table
|
30
|
+
end
|
31
|
+
|
32
|
+
def primary_key
|
33
|
+
@attr
|
34
|
+
end
|
35
|
+
|
36
|
+
def arel_primary_key
|
37
|
+
arel_table[@attr]
|
38
|
+
end
|
39
|
+
|
40
|
+
def connection
|
41
|
+
@model.connection
|
42
|
+
end
|
43
|
+
|
44
|
+
def mochigome_focus_settings
|
45
|
+
@focus_settings
|
46
|
+
end
|
47
|
+
|
48
|
+
def acts_as_mochigome_focus?
|
49
|
+
true
|
50
|
+
end
|
51
|
+
|
52
|
+
def all(options = {})
|
53
|
+
c = options[:conditions]
|
54
|
+
unless c.is_a?(Hash) && c.size == 1 && c[@attr].is_a?(Array)
|
55
|
+
raise QueryError.new("Invalid conditions given to SubgroupModel#all")
|
56
|
+
end
|
57
|
+
recs = c[@attr].map do |val|
|
58
|
+
SubgroupPseudoRecord.new(self, val)
|
59
|
+
end
|
60
|
+
# TODO: Support some kind of custom ordering
|
61
|
+
recs.sort!{|a,b| a.value <=> b.value}
|
62
|
+
recs
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
class SubgroupPseudoRecord
|
69
|
+
attr_reader :subgroup_model, :value
|
70
|
+
|
71
|
+
def initialize(subgroup_model, value)
|
72
|
+
@subgroup_model = subgroup_model
|
73
|
+
@value = value
|
74
|
+
end
|
75
|
+
|
76
|
+
def id
|
77
|
+
@value
|
78
|
+
end
|
79
|
+
|
80
|
+
def mochigome_focus
|
81
|
+
SubgroupPseudoRecordReportFocus.new(self)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class SubgroupPseudoRecordReportFocus
|
86
|
+
def initialize(rec)
|
87
|
+
@rec = rec
|
88
|
+
end
|
89
|
+
|
90
|
+
def type_name
|
91
|
+
@rec.subgroup_model.human_name
|
92
|
+
end
|
93
|
+
|
94
|
+
def name
|
95
|
+
@rec.value
|
96
|
+
end
|
97
|
+
|
98
|
+
def field_data
|
99
|
+
{}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/test/unit/data_node_test.rb
CHANGED
@@ -16,6 +16,15 @@ describe Mochigome::DataNode do
|
|
16
16
|
assert_equal "bar", datanode.name
|
17
17
|
end
|
18
18
|
|
19
|
+
it "can be duplicated deeply" do
|
20
|
+
datanode = Mochigome::DataNode.new(:foo, :bar)
|
21
|
+
twin = datanode.dup
|
22
|
+
datanode << Mochigome::DataNode.new(:xyzzy, :froboz)
|
23
|
+
assert_empty twin.children
|
24
|
+
datanode.name = "Mike"
|
25
|
+
assert_equal "bar", twin.name
|
26
|
+
end
|
27
|
+
|
19
28
|
describe "when created empty" do
|
20
29
|
before do
|
21
30
|
@datanode = Mochigome::DataNode.new(:data, :john_doe)
|
data/test/unit/query_test.rb
CHANGED
@@ -167,6 +167,29 @@ describe Mochigome::Query do
|
|
167
167
|
end
|
168
168
|
end
|
169
169
|
|
170
|
+
it "can subgroup layers by attributes" do
|
171
|
+
q = Mochigome::Query.new(
|
172
|
+
[Mochigome::SubgroupModel.new(Owner, :last_name), Owner, Store, Product]
|
173
|
+
)
|
174
|
+
data_node = q.run
|
175
|
+
assert_equal "Smith", (data_node/1).name
|
176
|
+
assert_equal "John Smith", (data_node/1/0).name
|
177
|
+
assert_equal "John's Store", (data_node/1/0/0).name
|
178
|
+
assert_equal "Product A", (data_node/1/0/0/0).name
|
179
|
+
end
|
180
|
+
|
181
|
+
it "can subgroup layers by attributes without including layer model" do
|
182
|
+
q = Mochigome::Query.new(
|
183
|
+
[Mochigome::SubgroupModel.new(Owner, :last_name), Store, Product]
|
184
|
+
)
|
185
|
+
data_node = q.run
|
186
|
+
assert_equal "Smith", (data_node/1).name
|
187
|
+
assert_equal "John's Store", (data_node/1/0).name
|
188
|
+
assert_equal "Product A", (data_node/1/0/0).name
|
189
|
+
end
|
190
|
+
|
191
|
+
# TODO: Test diamond patterns
|
192
|
+
|
170
193
|
it "collects aggregate data by grouping on all layers" do
|
171
194
|
q = Mochigome::Query.new(
|
172
195
|
[Owner, Store, Product],
|
@@ -190,6 +213,55 @@ describe Mochigome::Query do
|
|
190
213
|
assert_equal 2, (data_node/1/0/0)['Sales count']
|
191
214
|
end
|
192
215
|
|
216
|
+
it "collects aggregate data in subgroups" do
|
217
|
+
q = Mochigome::Query.new(
|
218
|
+
[Mochigome::SubgroupModel.new(Owner, :last_name), Owner, Store, Product],
|
219
|
+
:aggregate_sources => [[Product, Sale]]
|
220
|
+
)
|
221
|
+
data_node = q.run
|
222
|
+
|
223
|
+
assert_equal "Smith", (data_node/1).name
|
224
|
+
assert_equal 8, (data_node/1)['Sales count']
|
225
|
+
assert_equal "John Smith", (data_node/1/0).name
|
226
|
+
assert_equal 8, (data_node/1/0)['Sales count']
|
227
|
+
assert_equal "John's Store", (data_node/1/0/0).name
|
228
|
+
assert_equal 8, (data_node/1/0/0)['Sales count']
|
229
|
+
assert_equal "Product A", (data_node/1/0/0/0).name
|
230
|
+
assert_equal 5, (data_node/1/0/0/0)['Sales count']
|
231
|
+
end
|
232
|
+
|
233
|
+
it "collects aggregate data in subgroups going farther than layer list" do
|
234
|
+
q = Mochigome::Query.new(
|
235
|
+
[Mochigome::SubgroupModel.new(Owner, :last_name), Owner, Store],
|
236
|
+
:aggregate_sources => [[Product, Sale]]
|
237
|
+
)
|
238
|
+
data_node = q.run
|
239
|
+
|
240
|
+
assert_equal "Smith", (data_node/1).name
|
241
|
+
assert_equal 8, (data_node/1)['Sales count']
|
242
|
+
assert_equal "John Smith", (data_node/1/0).name
|
243
|
+
assert_equal 8, (data_node/1/0)['Sales count']
|
244
|
+
assert_equal "John's Store", (data_node/1/0/0).name
|
245
|
+
assert_equal 8, (data_node/1/0/0)['Sales count']
|
246
|
+
end
|
247
|
+
|
248
|
+
it "collects aggregate data using data model as focus if focus not supplied" do
|
249
|
+
q = Mochigome::Query.new(
|
250
|
+
[Owner, Store, Product],
|
251
|
+
:aggregate_sources => [Sale]
|
252
|
+
)
|
253
|
+
|
254
|
+
data_node = q.run
|
255
|
+
|
256
|
+
assert_equal "Jane's Store (North)", (data_node/1/0).name
|
257
|
+
assert_equal 11, (data_node/1/0)['Sales count']
|
258
|
+
|
259
|
+
assert_equal "Jane Doe", (data_node/1).name
|
260
|
+
assert_equal 16, (data_node/1)['Sales count']
|
261
|
+
|
262
|
+
assert_equal 24, data_node['Sales count']
|
263
|
+
end
|
264
|
+
|
193
265
|
it "collects aggregate data on layers above the focus" do
|
194
266
|
q = Mochigome::Query.new(
|
195
267
|
[Owner, Store, Product],
|
@@ -360,13 +432,6 @@ describe Mochigome::Query do
|
|
360
432
|
end
|
361
433
|
end
|
362
434
|
|
363
|
-
it "will not allow a query on targets of different types" do
|
364
|
-
q = Mochigome::Query.new([Owner, Store, Product])
|
365
|
-
assert_raises Mochigome::QueryError do
|
366
|
-
q.run([@store_x, @john])
|
367
|
-
end
|
368
|
-
end
|
369
|
-
|
370
435
|
it "will not allow a query on targets not in the layer list" do
|
371
436
|
q = Mochigome::Query.new([Product])
|
372
437
|
assert_raises Mochigome::QueryError do
|
@@ -413,5 +478,12 @@ describe Mochigome::Query do
|
|
413
478
|
assert_equal 1, q.instance_variable_get(:@ids_rel).to_sql.scan(/join .stores./i).size
|
414
479
|
end
|
415
480
|
|
481
|
+
it "complains if run given a condition on an unused table" do
|
482
|
+
q = Mochigome::Query.new([Product, Store])
|
483
|
+
assert_raises Mochigome::QueryError do
|
484
|
+
q.run(Arel::Table.new(Category.table_name)[:id].eq(41))
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
416
488
|
# TODO: Test that access filter join paths are followed, rather than closest path
|
417
489
|
end
|
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: 13
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 9
|
10
|
+
version: 0.0.9
|
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-03-
|
18
|
+
date: 2012-03-16 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
version_requirements: &id001 !ruby/object:Gem::Requirement
|
@@ -98,6 +98,7 @@ files:
|
|
98
98
|
- lib/mochigome_ver.rb
|
99
99
|
- lib/model_extensions.rb
|
100
100
|
- lib/query.rb
|
101
|
+
- lib/subgroup_model.rb
|
101
102
|
- test/app_root/app/controllers/application_controller.rb
|
102
103
|
- test/app_root/app/controllers/owners_controller.rb
|
103
104
|
- test/app_root/app/models/boring_datum.rb
|