mochigome 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|