mochigome 0.2.1 → 0.2.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/lib/mochigome_ver.rb +1 -1
- data/lib/model_extensions.rb +1 -1
- data/lib/query.rb +90 -74
- data/lib/subgroup_model.rb +1 -1
- data/test/app_root/app/models/owner.rb +3 -1
- data/test/unit/query_test.rb +35 -1
- metadata +4 -4
data/lib/mochigome_ver.rb
CHANGED
data/lib/model_extensions.rb
CHANGED
@@ -6,7 +6,7 @@ module Mochigome
|
|
6
6
|
base.write_inheritable_attribute :mochigome_focus_settings, nil
|
7
7
|
base.class_inheritable_reader :mochigome_focus_settings
|
8
8
|
|
9
|
-
# FIXME: Unclear on how this
|
9
|
+
# FIXME: Unclear on how this should interact with inheritance...
|
10
10
|
base.write_inheritable_attribute :mochigome_aggregation_settings_sets, {}
|
11
11
|
base.class_inheritable_reader :mochigome_aggregation_settings_sets
|
12
12
|
end
|
data/lib/query.rb
CHANGED
@@ -3,7 +3,8 @@ module Mochigome
|
|
3
3
|
def initialize(layers, options = {})
|
4
4
|
@name = options.delete(:root_name).try(:to_s) || "report"
|
5
5
|
access_filter = options.delete(:access_filter) || lambda {|cls| {}}
|
6
|
-
aggregate_sources = options.delete(:aggregate_sources) ||
|
6
|
+
aggregate_sources = options.delete(:aggregate_sources) || {}
|
7
|
+
@fieldsets = options.delete(:fieldsets) || {}
|
7
8
|
unless options.empty?
|
8
9
|
raise QueryError.new("Unknown options: #{options.keys.inspect}")
|
9
10
|
end
|
@@ -21,8 +22,32 @@ module Mochigome
|
|
21
22
|
layer_paths = [[]] if layer_paths.empty? # Create at least one QueryLine
|
22
23
|
@lines = layer_paths.map{ |path| QueryLine.new(path, access_filter) }
|
23
24
|
|
24
|
-
|
25
|
-
|
25
|
+
# Given a simple array of aggregates, just apply to every point
|
26
|
+
if aggregate_sources.is_a?(Array)
|
27
|
+
as_hash = ActiveSupport::OrderedHash.new
|
28
|
+
aggregate_sources.each do |a|
|
29
|
+
layer_paths.each do |path|
|
30
|
+
(0..path.size).each do |len|
|
31
|
+
key = [:report] + path.take(len)
|
32
|
+
as_hash[key] ||= []
|
33
|
+
as_hash[key] << a unless as_hash[key].include?(a)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
aggregate_sources = as_hash
|
38
|
+
end
|
39
|
+
|
40
|
+
aggregate_sources.each do |path, aggs|
|
41
|
+
models_path = path.drop(1) # Remove the :report symbol at the start
|
42
|
+
tgt_line = @lines.find do |line|
|
43
|
+
line.layer_path.take(models_path.size) == models_path
|
44
|
+
end
|
45
|
+
unless tgt_line
|
46
|
+
raise QueryError.new("Cannot find path #{path.inspect} for agg")
|
47
|
+
end
|
48
|
+
aggs.each do |a|
|
49
|
+
tgt_line.add_aggregate_source(models_path.size, a)
|
50
|
+
end
|
26
51
|
end
|
27
52
|
end
|
28
53
|
|
@@ -32,7 +57,7 @@ module Mochigome
|
|
32
57
|
@lines.each do |line|
|
33
58
|
tbl = line.build_id_table(cond)
|
34
59
|
parent_models = []
|
35
|
-
line.
|
60
|
+
line.layer_path.each do |model|
|
36
61
|
tbl.each do |ids_row|
|
37
62
|
i = ids_row["#{model.name}_id"]
|
38
63
|
if i
|
@@ -82,6 +107,7 @@ module Mochigome
|
|
82
107
|
def generate_datanodes(model_ids)
|
83
108
|
model_datanodes = {}
|
84
109
|
model_ids.keys.each do |model|
|
110
|
+
model_datanodes[model.name] ||= {}
|
85
111
|
# TODO: Find a way to do this without loading all recs at one time
|
86
112
|
model.all(
|
87
113
|
:conditions => {model.primary_key => model_ids[model].to_a},
|
@@ -89,10 +115,10 @@ module Mochigome
|
|
89
115
|
).each_with_index do |rec, seq_idx|
|
90
116
|
f = rec.mochigome_focus
|
91
117
|
dn = DataNode.new(f.type_name, f.name)
|
92
|
-
dn.merge!(f.field_data)
|
118
|
+
dn.merge!(f.field_data(@fieldsets[model]))
|
93
119
|
dn[:id] = rec.id
|
94
120
|
dn[:internal_type] = model.name
|
95
|
-
|
121
|
+
model_datanodes[model.name][rec.id] = [dn, seq_idx]
|
96
122
|
end
|
97
123
|
end
|
98
124
|
return model_datanodes
|
@@ -113,7 +139,7 @@ module Mochigome
|
|
113
139
|
# by the order of the records themselves.
|
114
140
|
# TODO: This way of getting model_idx could create problems
|
115
141
|
# if a class appears more than once in the tree.
|
116
|
-
model_idx = @lines.index{|line| line.
|
142
|
+
model_idx = @lines.index{|line| line.layer_path.any?{|m| m.name == model}}
|
117
143
|
(ordered_children[model_idx] ||= {})[seq_idx] = dn
|
118
144
|
end
|
119
145
|
ordered_children.keys.sort.each do |k|
|
@@ -130,7 +156,7 @@ module Mochigome
|
|
130
156
|
root.comment += "Report Generated: #{Time.now}\n"
|
131
157
|
@lines.each_with_index do |line, i|
|
132
158
|
root.comment += "Query Line #{i+1}:\n"
|
133
|
-
root.comment += " #{line.
|
159
|
+
root.comment += " #{line.layer_path.map(&:name).join("=>")}\n"
|
134
160
|
root.comment += " Join Paths:\n"
|
135
161
|
line.ids_rel.join_path_descriptions.each do |d|
|
136
162
|
root.comment += " #{d}\n"
|
@@ -143,21 +169,21 @@ module Mochigome
|
|
143
169
|
private
|
144
170
|
|
145
171
|
class QueryLine
|
146
|
-
attr_accessor :
|
172
|
+
attr_accessor :layer_path
|
147
173
|
attr_accessor :ids_rel
|
148
174
|
|
149
|
-
def initialize(
|
175
|
+
def initialize(layer_path, access_filter)
|
150
176
|
# TODO: Validate layer types: not empty, AR, act_as_mochigome_focus
|
151
|
-
@
|
177
|
+
@layer_path = layer_path
|
152
178
|
@access_filter = access_filter
|
153
179
|
|
154
|
-
@ids_rel = Relation.new(@
|
180
|
+
@ids_rel = Relation.new(@layer_path)
|
155
181
|
@ids_rel.apply_access_filter_func(@access_filter)
|
156
182
|
|
157
183
|
@aggregate_rels = ActiveSupport::OrderedHash.new
|
158
184
|
end
|
159
185
|
|
160
|
-
def add_aggregate_source(a)
|
186
|
+
def add_aggregate_source(depth, a)
|
161
187
|
focus_model, data_model, agg_setting_name = nil, nil, nil
|
162
188
|
if a.is_a?(Array) then
|
163
189
|
focus_model = a.select{|e| e.is_a?(Class)}.first
|
@@ -167,17 +193,19 @@ module Mochigome
|
|
167
193
|
focus_model = data_model = a
|
168
194
|
agg_setting_name = :default
|
169
195
|
end
|
170
|
-
#
|
171
|
-
|
172
|
-
agg_rel = Relation.new(@layer_types)
|
173
|
-
agg_rel.join_on_path_thru([focus_model, data_model])
|
174
|
-
agg_rel.apply_access_filter_func(@access_filter)
|
196
|
+
# TODO Raise exception if a isn't correctly formatted
|
175
197
|
|
176
|
-
|
198
|
+
agg_rel = Relation.new(@layer_path.take(depth))
|
199
|
+
key_cols = agg_rel.spine_layers.map{|m| m.arel_primary_key}
|
200
|
+
data_cols = key_cols + [data_model.arel_primary_key]
|
177
201
|
|
178
202
|
agg_fields = data_model.
|
179
203
|
mochigome_aggregation_settings(agg_setting_name).
|
180
204
|
options[:fields].reject{|a| a[:in_ruby]}
|
205
|
+
|
206
|
+
agg_rel.join_on_path_thru([focus_model, data_model])
|
207
|
+
agg_rel.apply_access_filter_func(@access_filter)
|
208
|
+
|
181
209
|
agg_fields.each_with_index do |a, i|
|
182
210
|
d_expr = a[:value_proc].call(data_model.arel_table)
|
183
211
|
d_expr = d_expr.expr if d_expr.respond_to?(:expr)
|
@@ -187,36 +215,34 @@ module Mochigome
|
|
187
215
|
agg_rel_key = {
|
188
216
|
:focus_model => focus_model,
|
189
217
|
:data_model => data_model,
|
190
|
-
:agg_setting_name => agg_setting_name
|
218
|
+
:agg_setting_name => agg_setting_name,
|
219
|
+
:depth => depth
|
191
220
|
}
|
192
221
|
|
193
|
-
@aggregate_rels[agg_rel_key] =
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
inner_rel.project(col.as("g%03u" % i)).group(col)
|
201
|
-
end
|
222
|
+
@aggregate_rels[agg_rel_key] = lambda {|cond|
|
223
|
+
inner_rel = agg_rel.dup
|
224
|
+
inner_rel.apply_condition(cond)
|
225
|
+
inner_query = inner_rel.to_arel
|
226
|
+
data_cols.each_with_index do |col, i|
|
227
|
+
inner_query.project(col.as("g%03u" % i)).group(col)
|
228
|
+
end
|
202
229
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
}
|
230
|
+
# FIXME: This subselect won't be necessary for all aggregation funcs.
|
231
|
+
# When we can avoid it, we should, because subselects are slow.
|
232
|
+
rel = Arel::SelectManager.new(
|
233
|
+
Arel::Table.engine,
|
234
|
+
Arel.sql("(#{inner_query.to_sql}) as mochigome_data")
|
235
|
+
)
|
236
|
+
d_tbl = Arel::Table.new("mochigome_data")
|
237
|
+
agg_fields.each_with_index do |a, i|
|
238
|
+
name = "d%03u" % i
|
239
|
+
rel.project(a[:agg_proc].call(d_tbl[name]).as(name))
|
240
|
+
end
|
241
|
+
key_cols.each_with_index do |col, i|
|
242
|
+
name = "g%03u" % i
|
243
|
+
rel.project(d_tbl[name].as(name)).group(name)
|
244
|
+
end
|
245
|
+
rel
|
220
246
|
}
|
221
247
|
end
|
222
248
|
|
@@ -230,7 +256,7 @@ module Mochigome
|
|
230
256
|
end
|
231
257
|
|
232
258
|
def build_id_table(cond)
|
233
|
-
if @
|
259
|
+
if @layer_path.empty?
|
234
260
|
return []
|
235
261
|
else
|
236
262
|
r = @ids_rel.clone
|
@@ -245,40 +271,34 @@ module Mochigome
|
|
245
271
|
end
|
246
272
|
|
247
273
|
def load_aggregate_data(node, cond)
|
248
|
-
@aggregate_rels.each do |key,
|
274
|
+
@aggregate_rels.each do |key, rel_func|
|
249
275
|
data_model = key[:data_model]
|
250
276
|
agg_name = key[:agg_setting_name]
|
251
277
|
agg_settings = data_model.mochigome_aggregation_settings(agg_name)
|
252
278
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
if group_values.empty?
|
260
|
-
data_tree = data_values
|
261
|
-
else
|
262
|
-
c = data_tree
|
263
|
-
group_values.take(group_values.size-1).each do |group_id|
|
264
|
-
c = (c[group_id] ||= {})
|
265
|
-
end
|
266
|
-
c[group_values.last] = data_values
|
267
|
-
end
|
268
|
-
end
|
269
|
-
insert_aggregate_data_fields(node, data_tree, agg_settings, 0)
|
279
|
+
q = rel_func.call(cond)
|
280
|
+
data_table = {}
|
281
|
+
connection.select_all(q.to_sql).each do |row|
|
282
|
+
group_values = row.keys.select{|k| k.start_with?("g")}.sort.map{|k| denilify(row[k])}
|
283
|
+
data_values = row.keys.select{|k| k.start_with?("d")}.sort.map{|k| row[k]}
|
284
|
+
data_table[group_values] = data_values
|
270
285
|
end
|
286
|
+
insert_aggregate_data_fields(node, data_table, agg_settings, [], key[:depth])
|
271
287
|
end
|
272
288
|
end
|
273
289
|
|
274
|
-
def insert_aggregate_data_fields(node, table, agg_settings,
|
275
|
-
|
276
|
-
|
290
|
+
def insert_aggregate_data_fields(node, table, agg_settings, path, tgt_depth)
|
291
|
+
# Ignore nodes inserted by other QueryLines
|
292
|
+
return unless path.size == 0 || node[:internal_type] == @layer_path[path.size-1].name
|
293
|
+
|
294
|
+
if path.size == tgt_depth
|
277
295
|
fields = agg_settings.options[:fields]
|
278
296
|
# Pre-fill the node with default values in the right order
|
279
297
|
fields.each{|fld| node[fld[:name]] = fld[:default] unless fld[:hidden] }
|
280
298
|
agg_row = {} # Hold regular results here to be used in ruby-based fields
|
281
|
-
|
299
|
+
node_data = table[path]
|
300
|
+
return unless node_data
|
301
|
+
fields.reject{|fld| fld[:in_ruby]}.zip(node_data).each do |fld, v|
|
282
302
|
v ||= fld[:default]
|
283
303
|
agg_row[fld[:name]] = v
|
284
304
|
node[fld[:name]] = v unless fld[:hidden]
|
@@ -286,13 +306,9 @@ module Mochigome
|
|
286
306
|
fields.select{|fld| fld[:in_ruby]}.each do |fld|
|
287
307
|
node[fld[:name]] = fld[:ruby_proc].call(agg_row)
|
288
308
|
end
|
289
|
-
node.children.each do |c|
|
290
|
-
insert_aggregate_data_fields(c, [], agg_settings, depth+1)
|
291
|
-
end
|
292
309
|
else
|
293
310
|
node.children.each do |c|
|
294
|
-
|
295
|
-
insert_aggregate_data_fields(c, subtable, agg_settings, depth+1)
|
311
|
+
insert_aggregate_data_fields(c, table, agg_settings, path + [c[:id]], tgt_depth)
|
296
312
|
end
|
297
313
|
end
|
298
314
|
end
|
data/lib/subgroup_model.rb
CHANGED
data/test/unit/query_test.rb
CHANGED
@@ -12,7 +12,8 @@ describe Mochigome::Query do
|
|
12
12
|
|
13
13
|
@product_e = create(:product, :name => "Product E") # No category
|
14
14
|
|
15
|
-
@john = create(:owner, :first_name => "John", :last_name => "Smith"
|
15
|
+
@john = create(:owner, :first_name => "John", :last_name => "Smith",
|
16
|
+
:birth_date => 25.years.ago)
|
16
17
|
@store_x = create(:store, :name => "John's Store", :owner => @john)
|
17
18
|
|
18
19
|
@jane = create(:owner, :first_name => "Jane", :last_name => "Doe",
|
@@ -654,6 +655,7 @@ describe Mochigome::Query do
|
|
654
655
|
:aggregate_sources => [Sale]
|
655
656
|
)
|
656
657
|
dn = q.run
|
658
|
+
assert_equal 24, dn['Sales count']
|
657
659
|
assert_equal "John Smith", (dn/0).name
|
658
660
|
assert_equal 8, (dn/0)['Sales count']
|
659
661
|
assert_equal "John's Store", (dn/0/0).name
|
@@ -661,4 +663,36 @@ describe Mochigome::Query do
|
|
661
663
|
assert_equal "Category 1", (dn/0/1).name
|
662
664
|
assert_equal 5, (dn/0/1)['Sales count']
|
663
665
|
end
|
666
|
+
|
667
|
+
it "can limit aggregations to certain points on the layer tree" do
|
668
|
+
q = Mochigome::Query.new(
|
669
|
+
{:report => {Owner => [Store, Category]}},
|
670
|
+
:aggregate_sources => {
|
671
|
+
[:report] => [Sale],
|
672
|
+
[:report, Owner, Store] => [Sale]
|
673
|
+
}
|
674
|
+
)
|
675
|
+
dn = q.run
|
676
|
+
assert_equal 24, dn['Sales count']
|
677
|
+
assert_equal "John Smith", (dn/0).name
|
678
|
+
refute (dn/0).has_key?('Sales count')
|
679
|
+
assert_equal "John's Store", (dn/0/0).name
|
680
|
+
assert_equal 8, (dn/0/0)['Sales count']
|
681
|
+
assert_equal "Category 1", (dn/0/1).name
|
682
|
+
refute (dn/0/1).has_key?('Sales count')
|
683
|
+
end
|
684
|
+
|
685
|
+
it "can specify which fieldsets to use for particular models" do
|
686
|
+
q = Mochigome::Query.new(
|
687
|
+
[Owner],
|
688
|
+
:fieldsets => {
|
689
|
+
Owner => [:default, :age]
|
690
|
+
}
|
691
|
+
)
|
692
|
+
dn = q.run
|
693
|
+
assert_equal "John Smith", (dn/0).name
|
694
|
+
assert_equal 25, (dn/0)["age"]
|
695
|
+
assert_equal "Jane Doe", (dn/1).name
|
696
|
+
assert_equal 30.years.ago.to_date, (dn/1)["birth_date"]
|
697
|
+
end
|
664
698
|
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: 19
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 2
|
9
|
-
-
|
10
|
-
version: 0.2.
|
9
|
+
- 2
|
10
|
+
version: 0.2.2
|
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-
|
18
|
+
date: 2012-10-03 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
version_requirements: &id001 !ruby/object:Gem::Requirement
|