mochigome 0.0.3 → 0.0.4

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