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 CHANGED
@@ -1,3 +1,3 @@
1
1
  module Mochigome
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.2"
3
3
  end
@@ -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 interacts with inheritance...
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
- aggregate_sources.each do |a|
25
- @lines.each{|line| line.add_aggregate_source(a)}
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.layer_types.each do |model|
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
- (model_datanodes[model.name] ||= {})[rec.id] = [dn, seq_idx]
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.layer_types.any?{|m| m.name == model}}
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.layer_types.map(&:name).join("=>")}\n"
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 :layer_types
172
+ attr_accessor :layer_path
147
173
  attr_accessor :ids_rel
148
174
 
149
- def initialize(layer_types, access_filter)
175
+ def initialize(layer_path, access_filter)
150
176
  # TODO: Validate layer types: not empty, AR, act_as_mochigome_focus
151
- @layer_types = layer_types
177
+ @layer_path = layer_path
152
178
  @access_filter = access_filter
153
179
 
154
- @ids_rel = Relation.new(@layer_types)
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
- # FIXME Raise exception if a isn't in a correct format
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
- key_cols = @ids_rel.spine_layers.map{|m| m.arel_primary_key}
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] = (0..key_cols.length).map{|n|
194
- lambda {|cond|
195
- data_rel = agg_rel.clone
196
- data_rel.apply_condition(cond)
197
- data_cols = key_cols.take(n) + [data_model.arel_primary_key]
198
- inner_rel = data_rel.to_arel
199
- data_cols.each_with_index do |col, i|
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
- # FIXME: This subtable won't be necessary for all aggregation funcs.
204
- # When we can avoid it, we should, for performance.
205
- rel = Arel::SelectManager.new(
206
- Arel::Table.engine,
207
- Arel.sql("(#{inner_rel.to_sql}) as mochigome_data")
208
- )
209
- d_tbl = Arel::Table.new("mochigome_data")
210
- agg_fields.each_with_index do |a, i|
211
- name = "d%03u" % i
212
- rel.project(a[:agg_proc].call(d_tbl[name]).as(name))
213
- end
214
- key_cols.take(n).each_with_index do |col, i|
215
- name = "g%03u" % i
216
- rel.project(d_tbl[name].as(name)).group(name)
217
- end
218
- rel
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 @layer_types.empty?
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, rel_funcs|
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
- rel_funcs.each do |rel_func|
254
- q = rel_func.call(cond)
255
- data_tree = {}
256
- connection.select_all(q.to_sql).each do |row|
257
- group_values = row.keys.select{|k| k.start_with?("g")}.sort.map{|k| denilify(row[k])}
258
- data_values = row.keys.select{|k| k.start_with?("d")}.sort.map{|k| row[k]}
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, depth)
275
- return unless depth == 0 || node[:internal_type] == @layer_types[depth-1].name
276
- if table.is_a? Array
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
- fields.reject{|fld| fld[:in_ruby]}.zip(table).each do |fld, v|
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
- subtable = table[c[:id]] || []
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
@@ -118,7 +118,7 @@ module Mochigome
118
118
  @rec.value
119
119
  end
120
120
 
121
- def field_data
121
+ def field_data(fieldset_names = nil)
122
122
  {}
123
123
  end
124
124
  end
@@ -1,5 +1,7 @@
1
1
  class Owner < ActiveRecord::Base
2
- acts_as_mochigome_focus
2
+ acts_as_mochigome_focus do |f|
3
+ f.fieldset :age, ["birth_date", "age"]
4
+ end
3
5
 
4
6
  has_many :stores
5
7
 
@@ -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: 21
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 2
9
- - 1
10
- version: 0.2.1
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-09-27 00:00:00 Z
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