mochigome 0.0.8 → 0.0.9
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/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
|