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 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