mochigome 0.0.3 → 0.0.4

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.
@@ -3,12 +3,9 @@ require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
3
3
  describe Mochigome::Query do
4
4
  before do
5
5
  @category1 = create(:category, :name => "Category 1")
6
- @product_a = create(:product, :name => "Product A", :category => @category1)
6
+ @product_a = create(:product, :name => "Product A", :category => @category1, :price => 5)
7
7
  @product_b = create(:product, :name => "Product B", :category => @category1)
8
8
 
9
- # Belongs to a category, but fails Category's has_many(:products) conditions
10
- @product_x = create(:product, :name => "Product X", :category => @category1, :categorized => false)
11
-
12
9
  @category2 = create(:category, :name => "Category 2")
13
10
  @product_c = create(:product, :name => "Product C", :category => @category2)
14
11
  @product_d = create(:product, :name => "Product D", :category => @category2)
@@ -43,10 +40,20 @@ describe Mochigome::Query do
43
40
  end
44
41
  end
45
42
 
46
- # Convenience function to check DataSet output validity
47
- def assert_equal_objs(a, b)
43
+ after do
44
+ Category.delete_all
45
+ Product.delete_all
46
+ Owner.delete_all
47
+ Store.delete_all
48
+ StoreProduct.delete_all
49
+ Sale.delete_all
50
+ end
51
+
52
+ # Convenience functions to check DataNode output validity
53
+
54
+ def assert_equal_children(a, node)
55
+ b = node.children
48
56
  assert_equal a.size, b.size
49
- # Not checking aggregate data because we don't know about a's context here
50
57
  a.zip(b).each do |obj, fields|
51
58
  obj.mochigome_focus.field_data.each do |k,v|
52
59
  assert_equal v, fields[k]
@@ -54,60 +61,93 @@ describe Mochigome::Query do
54
61
  end
55
62
  end
56
63
 
57
- it "returns an empty DataNode if no objects given" do
64
+ def assert_no_children(obj)
65
+ assert_empty obj.children
66
+ end
67
+
68
+ it "returns an empty DataNode if given an empty array" do
58
69
  q = Mochigome::Query.new([Category, Product])
59
70
  data_node = q.run([])
60
71
  assert_empty data_node
61
- assert_empty data_node.children
72
+ assert_no_children data_node
62
73
  end
63
74
 
64
- it "can build a one-layer DataNode" do
75
+ it "returns all possible results if no conditions given" do
76
+ q = Mochigome::Query.new([Category, Product])
77
+ data_node = q.run()
78
+ assert_equal 2, data_node.children.size
79
+ assert_equal 4, (data_node/0).children.size + (data_node/1).children.size
80
+ end
81
+
82
+ it "can build a one-layer DataNode given an object with an id to focus on" do
65
83
  q = Mochigome::Query.new([Product])
66
84
  data_node = q.run(@product_a)
67
- assert_equal_objs [@product_a], data_node.children
68
- assert_empty data_node.children[0].children
85
+ assert_equal_children [@product_a], data_node
86
+ assert_no_children data_node/0
87
+ end
88
+
89
+ it "can build a one-layer DataNode when given an arbitrary Arel condition" do
90
+ q = Mochigome::Query.new([Product])
91
+ tbl = Arel::Table.new(Product.table_name)
92
+ data_node = q.run(tbl[:name].eq(@product_a.name))
93
+ assert_equal_children [@product_a], data_node
94
+ assert_no_children data_node/0
95
+ end
96
+
97
+ it "orders by ID by default" do
98
+ q = Mochigome::Query.new([Product])
99
+ data_node = q.run([@product_b, @product_a, @product_c])
100
+ assert_equal_children [@product_a, @product_b, @product_c], data_node
101
+ end
102
+
103
+ it "orders by custom fields when the model focus settings specify so" do
104
+ q = Mochigome::Query.new([Category])
105
+ catZ = create(:category, :name => "Zebras") # Created first, has lower ID
106
+ catA = create(:category, :name => "Apples")
107
+ data_node = q.run([catZ, catA])
108
+ assert_equal catA.name, (data_node/0).name
109
+ assert_equal catZ.name, (data_node/1).name
69
110
  end
70
111
 
71
112
  it "uses the model focus's type name for the DataNode's type name" do
72
113
  q = Mochigome::Query.new([Store])
73
114
  data_node = q.run(@store_x)
74
- assert_equal "Storefront", data_node.children[0].type_name.to_s
115
+ assert_equal "Storefront", (data_node/0).type_name.to_s
75
116
  end
76
117
 
77
118
  it "adds an internal_type attribute containing the model class's name" do
78
119
  q = Mochigome::Query.new([Store])
79
120
  data_node = q.run(@store_x)
80
- assert_equal "Store", data_node.children[0][:internal_type]
121
+ assert_equal "Store", (data_node/0)[:internal_type]
81
122
  end
82
123
 
83
124
  it "can build a two-layer tree from a record with a belongs_to association" do
84
125
  q = Mochigome::Query.new([Category, Product])
85
126
  data_node = q.run(@product_a)
86
- assert_equal_objs [@category1], data_node.children
87
- assert_equal_objs [@product_a], data_node.children[0].children
88
- assert_empty data_node.children[0].children[0].children
127
+ assert_equal_children [@category1], data_node
128
+ assert_equal_children [@product_a], data_node/0
129
+ assert_no_children data_node/0/0
89
130
  end
90
131
 
91
132
  it "can build a two-layer tree from an array of records in the second layer" do
92
133
  q = Mochigome::Query.new([Category, Product])
93
134
  data_node = q.run([@product_a, @product_d, @product_b])
94
- assert_equal_objs [@category1, @category2], data_node.children
95
- assert_equal_objs [@product_a, @product_b], data_node.children[0].children
96
- assert_equal_objs [@product_d], data_node.children[1].children
135
+ assert_equal_children [@category1, @category2], data_node
136
+ assert_equal_children [@product_a, @product_b], data_node/0
137
+ assert_equal_children [@product_d], data_node/1
97
138
  end
98
139
 
99
140
  it "can build a two-layer tree from a record with a has_many association" do
100
141
  q = Mochigome::Query.new([Category, Product])
101
142
  data_node = q.run(@category1)
102
- assert_equal_objs [@category1], data_node.children
103
- assert_equal_objs [@product_a, @product_b], data_node.children[0].children
104
- assert_empty data_node.children[0].children[0].children
143
+ assert_equal_children [@category1], data_node
144
+ assert_equal_children [@product_a, @product_b], data_node/0
145
+ assert_no_children data_node/0/0
105
146
  end
106
147
 
107
- it "cannot build a DataNode tree when given disconnected layers" do
108
- q = Mochigome::Query.new([Category, BoringDatum])
148
+ it "cannot build a Query through disconnected layers" do
109
149
  assert_raises Mochigome::QueryError do
110
- data_node = q.run(@category1)
150
+ q = Mochigome::Query.new([Category, BoringDatum])
111
151
  end
112
152
  end
113
153
 
@@ -119,70 +159,158 @@ describe Mochigome::Query do
119
159
  [@product_a, @product_b, @product_c, @product_d, @product_e]
120
160
  ].each do |tgt|
121
161
  data_node = q.run(tgt)
122
- assert_equal_objs [@john, @jane], data_node.children
123
- assert_equal_objs [@store_x], data_node.children[0].children
124
- assert_equal_objs [@store_y, @store_z], data_node.children[1].children
125
- assert_equal_objs [@product_a, @product_c],
126
- data_node.children[0].children[0].children
127
- assert_equal_objs [@product_c, @product_d],
128
- data_node.children[1].children[1].children
162
+ assert_equal_children [@john, @jane], data_node
163
+ assert_equal_children [@store_x], data_node/0
164
+ assert_equal_children [@store_y, @store_z], data_node/1
165
+ assert_equal_children [@product_a, @product_c], data_node/0/0
166
+ assert_equal_children [@product_c, @product_d], data_node/1/1
129
167
  end
130
168
  end
131
169
 
132
- it "collects aggregate data in the context of all layers when traversing down" do
133
- q = Mochigome::Query.new([Owner, Store, Product])
170
+ it "collects aggregate data by grouping on all layers" do
171
+ q = Mochigome::Query.new(
172
+ [Owner, Store, Product],
173
+ :aggregate_sources => [[Product, Sale]]
174
+ )
175
+
134
176
  data_node = q.run([@john, @jane])
135
177
  # Store X, Product C
136
- assert_equal "Product C", data_node.children[0].children[0].children[1].name
137
- assert_equal 3, data_node.children[0].children[0].children[1]['Sales count']
178
+ assert_equal "Product C", (data_node/0/0/1).name
179
+ assert_equal 3, (data_node/0/0/1)['Sales count']
138
180
  # Store Z, Product C
139
- assert_equal "Product C", data_node.children[1].children[1].children[0].name
140
- assert_equal 2, data_node.children[1].children[1].children[0]['Sales count']
141
- end
181
+ assert_equal "Product C", (data_node/1/1/0).name
182
+ assert_equal 2, (data_node/1/1/0)['Sales count']
142
183
 
143
- it "collects aggregate data in the context of all layers when traversing up" do
144
- q = Mochigome::Query.new([Owner, Store, Product])
145
184
  data_node = q.run(@product_c)
146
185
  # Store X, Product C
147
- assert_equal "Product C", data_node.children[0].children[0].children[0].name
148
- assert_equal 3, data_node.children[0].children[0].children[0]['Sales count']
186
+ assert_equal "Product C", (data_node/0/0/0).name
187
+ assert_equal 3, (data_node/0/0/0)['Sales count']
149
188
  # Store Z, Product C
150
- assert_equal "Product C", data_node.children[1].children[0].children[0].name
151
- assert_equal 2, data_node.children[1].children[0].children[0]['Sales count']
189
+ assert_equal "Product C", (data_node/1/0/0).name
190
+ assert_equal 2, (data_node/1/0/0)['Sales count']
191
+ end
192
+
193
+ it "collects aggregate data on layers above the focus" do
194
+ q = Mochigome::Query.new(
195
+ [Owner, Store, Product],
196
+ :aggregate_sources => [[Product, Sale]]
197
+ )
198
+
199
+ data_node = q.run([@john, @jane])
200
+
201
+ assert_equal "Jane's Store (North)", (data_node/1/0).name
202
+ assert_equal 11, (data_node/1/0)['Sales count']
203
+
204
+ assert_equal "Jane Doe", (data_node/1).name
205
+ assert_equal 16, (data_node/1)['Sales count']
206
+
207
+ assert_equal 24, data_node['Sales count']
152
208
  end
153
209
 
154
- it "collects aggregate data in the context of distant layers" do
155
- # TODO: Implement me! I think this is necessary to justify focus_data_node_objs passing obj_stack
210
+ it "can do conditional counts" do
211
+ q = Mochigome::Query.new(
212
+ [Category],
213
+ :aggregate_sources => [[Category, Product]]
214
+ )
215
+ data_node = q.run([@category1, @category2])
216
+ assert_equal 1, (data_node/0)['Expensive products']
217
+ assert_equal 2, (data_node/1)['Expensive products']
156
218
  end
157
219
 
220
+ it "can do sums" do
221
+ q = Mochigome::Query.new(
222
+ [Owner, Store],
223
+ :aggregate_sources => [[Store, Product]]
224
+ )
225
+ data_node = q.run([@john])
226
+ assert_equal (@product_a.price + @product_c.price),
227
+ data_node['Products sum price']
228
+ end
229
+
230
+ it "still does conditional counts correctly when joins below focus used" do
231
+ af = proc do |cls|
232
+ return {} unless cls == Product
233
+ return {
234
+ :join_paths => [[Product, StoreProduct, Store]]
235
+ }
236
+ end
237
+ q = Mochigome::Query.new(
238
+ [Category],
239
+ :aggregate_sources => [[Category, Product]],
240
+ :access_filter => af
241
+ )
242
+ data_node = q.run([@category1, @category2])
243
+ assert_equal 1, (data_node/0)['Expensive products']
244
+ assert_equal 2, (data_node/1)['Expensive products']
245
+ end
246
+
247
+ it "still does sums correctly when joins below focus are used" do
248
+ af = proc do |cls|
249
+ return {} unless cls == Store
250
+ return {
251
+ :join_paths => [[Store, StoreProduct, Sale]]
252
+ }
253
+ end
254
+ q = Mochigome::Query.new(
255
+ [Owner, Store],
256
+ :aggregate_sources => [[Store, Product]],
257
+ :access_filter => af
258
+ )
259
+ data_node = q.run([@john])
260
+ assert_equal (@product_a.price + @product_c.price),
261
+ data_node['Products sum price']
262
+ end
263
+
264
+ it "does not include hidden aggregation fields in output" do
265
+ q = Mochigome::Query.new(
266
+ [Owner, Store],
267
+ :aggregate_sources => [[Store, Product]]
268
+ )
269
+ data_node = q.run([@john])
270
+ refute data_node.has_key?('Secret count')
271
+ end
272
+
273
+ it "correctly runs aggregation fields implemented in ruby" do
274
+ q = Mochigome::Query.new(
275
+ [Owner, Store],
276
+ :aggregate_sources => [[Store, Product]]
277
+ )
278
+ data_node = q.run([@john])
279
+ assert_equal 4, data_node["Count squared"]
280
+ end
281
+
282
+ # TODO: Test case where data model is already in layer path
283
+ # TODO: Test case where the condition is deeper than the focus model
284
+ # TODO: Test use of non-trivial function for aggregation value
285
+
158
286
  it "puts a comment on the root node describing the query" do
159
287
  q = Mochigome::Query.new([Owner, Store, Product])
160
288
  data_node = q.run([@store_x, @store_y, @store_z])
161
289
  c = data_node.comment
162
290
  assert_match c, /^Mochigome Version: #{Mochigome::VERSION}\n/
163
- assert_match c, /\nTime: \w{3} \w{3} \d+ .+\n/
291
+ assert_match c, /\nReport Generated: \w{3} \w{3} \d+ .+\n/
164
292
  assert_match c, /\nLayers: Owner => Store => Product\n/
165
- assert_match c, /\nAR Association Path:\n\* <- Owner.+\n\* == Store.+\n\* -> Product.+\n/
293
+ assert_match c, /\nAR Path: Owner => Store => StoreProduct => Product\n/
166
294
  end
167
295
 
168
- it "puts a descriptive comment on the first node of each layer" do
296
+ it "names the root node 'report' by default" do
169
297
  q = Mochigome::Query.new([Owner, Store, Product])
170
298
  data_node = q.run([@store_x, @store_y, @store_z])
299
+ assert_equal "report", data_node.name
300
+ end
171
301
 
172
- owner_comment = data_node.children[0].comment
173
- assert owner_comment
174
- assert_nil data_node.children[1].comment # No comment on second owner
175
-
176
- store_comment = data_node.children[0].children[0].comment
177
- assert store_comment
178
- assert_nil data_node.children[1].children[0].comment # No comment on second store
179
-
180
- product_comment = data_node.children[0].children[0].children[0].comment
181
- assert product_comment
182
- assert_nil data_node.children[0].children[0].children[1].comment # No comment on 2nd product
302
+ it "can set the root node's name to a provided value" do
303
+ q = Mochigome::Query.new(
304
+ [Owner, Store, Product],
305
+ :root_name => "cheese"
306
+ )
307
+ data_node = q.run([@store_x, @store_y, @store_z])
308
+ assert_equal "cheese", data_node.name
309
+ end
183
310
 
184
- [owner_comment, store_comment, product_comment].each do |comment|
185
- assert_match comment, /^Context:\nOwner:#{@john.id}.*\n/ # Owner is always in context
311
+ it "will complain if initialized with an unknown option" do
312
+ assert_raises Mochigome::QueryError do
313
+ q = Mochigome::Query.new([Owner, Store, Product], :flim_flam => 123)
186
314
  end
187
315
  end
188
316
 
@@ -199,4 +327,45 @@ describe Mochigome::Query do
199
327
  q.run(@category1)
200
328
  end
201
329
  end
330
+
331
+ it "can use a provided access filter function to limit query results" do
332
+ af = proc do |cls|
333
+ return {} unless cls == Product
334
+ return {
335
+ :condition => Arel::Table.new(Product.table_name)[:category_id].gt(0)
336
+ }
337
+ end
338
+ q = Mochigome::Query.new([Product], :access_filter => af)
339
+ dn = q.run(Product.all) # FIXME: Need a better way of doing "all objs" queries
340
+ assert_equal 4, dn.children.size
341
+ refute dn.children.any?{|c| c.name == "Product E"}
342
+ end
343
+
344
+ it "can do joins at the request of an access filter" do
345
+ af = proc do |cls|
346
+ return {} unless cls == Product
347
+ return {
348
+ :join_paths => [[Product, StoreProduct, Store]],
349
+ :condition => Arel::Table.new(Store.table_name)[:name].matches("Jo%")
350
+ }
351
+ end
352
+ q = Mochigome::Query.new([Product], :access_filter => af)
353
+ dn = q.run(Product.all) # FIXME: Need a better way of doing "all objs" queries
354
+ assert_equal 2, dn.children.size
355
+ refute dn.children.any?{|c| c.name == "Product E"}
356
+ end
357
+
358
+ it "access filter joins will not duplicate joins already in the query" do
359
+ af = proc do |cls|
360
+ return {} unless cls == Product
361
+ return {
362
+ :join_paths => [[Product, StoreProduct, Store]],
363
+ :condition => Arel::Table.new(Store.table_name)[:name].matches("Jo%")
364
+ }
365
+ end
366
+ q = Mochigome::Query.new([Product, Store], :access_filter => af)
367
+ assert_equal 1, q.instance_variable_get(:@ids_rel).to_sql.scan(/join .stores./i).size
368
+ end
369
+
370
+ # TODO: Test that access filter join paths are followed, rather than closest path
202
371
  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: 25
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 3
10
- version: 0.0.3
9
+ - 4
10
+ version: 0.0.4
11
11
  platform: ruby
12
12
  authors:
13
13
  - David Mike Simon
@@ -15,10 +15,25 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-11-14 00:00:00 Z
18
+ date: 2012-03-02 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
- requirement: &id001 !ruby/object:Gem::Requirement
21
+ version_requirements: &id001 !ruby/object:Gem::Requirement
22
+ none: false
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ hash: 1
27
+ segments:
28
+ - 2
29
+ - 1
30
+ version: "2.1"
31
+ requirement: *id001
32
+ prerelease: false
33
+ type: :runtime
34
+ name: arel
35
+ - !ruby/object:Gem::Dependency
36
+ version_requirements: &id002 !ruby/object:Gem::Requirement
22
37
  none: false
23
38
  requirements:
24
39
  - - ">="
@@ -27,12 +42,12 @@ dependencies:
27
42
  segments:
28
43
  - 0
29
44
  version: "0"
30
- version_requirements: *id001
31
- name: ruport
45
+ requirement: *id002
32
46
  prerelease: false
33
47
  type: :runtime
48
+ name: ruport
34
49
  - !ruby/object:Gem::Dependency
35
- requirement: &id002 !ruby/object:Gem::Requirement
50
+ version_requirements: &id003 !ruby/object:Gem::Requirement
36
51
  none: false
37
52
  requirements:
38
53
  - - ">="
@@ -41,12 +56,12 @@ dependencies:
41
56
  segments:
42
57
  - 0
43
58
  version: "0"
44
- version_requirements: *id002
45
- name: nokogiri
59
+ requirement: *id003
46
60
  prerelease: false
47
61
  type: :runtime
62
+ name: nokogiri
48
63
  - !ruby/object:Gem::Dependency
49
- requirement: &id003 !ruby/object:Gem::Requirement
64
+ version_requirements: &id004 !ruby/object:Gem::Requirement
50
65
  none: false
51
66
  requirements:
52
67
  - - ">="
@@ -55,11 +70,11 @@ dependencies:
55
70
  segments:
56
71
  - 0
57
72
  version: "0"
58
- version_requirements: *id003
59
- name: rgl
73
+ requirement: *id004
60
74
  prerelease: false
61
75
  type: :runtime
62
- description: Mochigome builds sophisticated report datasets from your ActiveRecord models
76
+ name: rgl
77
+ description: Report generator that uses your ActiveRecord associations
63
78
  email: david.mike.simon@gmail.com
64
79
  executables: []
65
80
 
@@ -75,6 +90,7 @@ files:
75
90
  - README.rdoc
76
91
  - Rakefile
77
92
  - TODO
93
+ - lib/arel_rails2_hacks.rb
78
94
  - lib/data_node.rb
79
95
  - lib/exceptions.rb
80
96
  - lib/mochigome.rb
@@ -96,11 +112,13 @@ files:
96
112
  - test/app_root/config/database.yml
97
113
  - test/app_root/config/environment.rb
98
114
  - test/app_root/config/environments/test.rb
115
+ - test/app_root/config/initializers/arel.rb
99
116
  - test/app_root/config/offroad.yml
100
117
  - test/app_root/config/preinitializer.rb
101
118
  - test/app_root/config/routes.rb
102
119
  - test/app_root/db/migrate/20110817163830_create_tables.rb
103
120
  - test/app_root/vendor/plugins/mochigome/init.rb
121
+ - test/console.sh
104
122
  - test/factories.rb
105
123
  - test/test.watchr
106
124
  - test/test_helper.rb