ardm-aggregates 1.2.0

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.
@@ -0,0 +1,13 @@
1
+ module DataMapper
2
+ module Aggregates
3
+ module Model
4
+ include Functions
5
+
6
+ private
7
+
8
+ def property_by_name(property_name)
9
+ properties(repository.name)[property_name]
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ module DataMapper
2
+ module Aggregates
3
+ module Operators
4
+ def count
5
+ DataMapper::Query::Operator.new(self, :count)
6
+ end
7
+
8
+ def min
9
+ DataMapper::Query::Operator.new(self, :min)
10
+ end
11
+
12
+ def max
13
+ DataMapper::Query::Operator.new(self, :max)
14
+ end
15
+
16
+ def avg
17
+ DataMapper::Query::Operator.new(self, :avg)
18
+ end
19
+
20
+ def sum
21
+ DataMapper::Query::Operator.new(self, :sum)
22
+ end
23
+ end # module Operators
24
+ end # module Aggregates
25
+ end # module DataMapper
@@ -0,0 +1,27 @@
1
+ module DataMapper
2
+ module Aggregates
3
+ module Query
4
+ def self.included(base)
5
+ base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
6
+ # FIXME: figure out a cleaner approach than AMC
7
+ alias_method :assert_valid_fields_without_operator, :assert_valid_fields
8
+ alias_method :assert_valid_fields, :assert_valid_fields_with_operator
9
+ RUBY
10
+ end
11
+
12
+ def assert_valid_fields_with_operator(fields, unique)
13
+ operators, fields = fields.partition { |f| f.kind_of?(DataMapper::Query::Operator) }
14
+
15
+ operators.each do |operator|
16
+ target = operator.target
17
+
18
+ unless target == :all || @properties.include?(target)
19
+ raise ArgumentError, "+options[:fields]+ entry #{target.inspect} does not map to a property in #{model}"
20
+ end
21
+ end
22
+
23
+ assert_valid_fields_without_operator(fields, unique)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,13 @@
1
+ module DataMapper
2
+ module Aggregates
3
+ module Repository
4
+ def aggregate(query)
5
+ unless query.valid?
6
+ []
7
+ else
8
+ adapter.aggregate(query)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ module DataMapper
2
+ module Aggregates
3
+ VERSION = '1.2.0'
4
+ end
5
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec'
2
+ require 'isolated/require_spec'
3
+ require 'dm-core/spec/setup'
4
+
5
+ # To really test this behavior, this spec needs to be run in isolation and not
6
+ # as part of the typical rake spec run, which requires dm-aggregates upfront
7
+
8
+ if %w[ postgres mysql sqlite oracle sqlserver ].include?(ENV['ADAPTER'])
9
+
10
+ describe "require 'dm-aggregates after calling DataMapper.setup" do
11
+
12
+ before(:all) do
13
+ @adapter = DataMapper::Spec.adapter
14
+ require 'dm-aggregates'
15
+ end
16
+
17
+ it_should_behave_like "require 'dm-aggregates'"
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec'
2
+ require 'isolated/require_spec'
3
+ require 'dm-core/spec/setup'
4
+
5
+ # To really test this behavior, this spec needs to be run in isolation and not
6
+ # as part of the typical rake spec run, which requires dm-aggregates upfront
7
+
8
+ if %w[ postgres mysql sqlite oracle sqlserver ].include?(ENV['ADAPTER'])
9
+
10
+ describe "require 'dm-aggregates' before calling DataMapper.setup" do
11
+
12
+ before(:all) do
13
+ require 'dm-aggregates'
14
+ @adapter = DataMapper::Spec.adapter
15
+ end
16
+
17
+ it_should_behave_like "require 'dm-aggregates'"
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,13 @@
1
+ shared_examples_for "require 'dm-aggregates'" do
2
+
3
+ %w[ Repository Model Collection Query ].each do |name|
4
+ it "should include the aggregate api in DataMapper::#{name}" do
5
+ (DataMapper.const_get(name) < DataMapper::Aggregates.const_get(name)).should be(true)
6
+ end
7
+ end
8
+
9
+ it "should include the aggregate api into the adapter" do
10
+ @adapter.respond_to?(:aggregate).should be(true)
11
+ end
12
+
13
+ end
@@ -0,0 +1,125 @@
1
+ require 'spec_helper'
2
+
3
+ describe DataMapper::Collection do
4
+ supported_by :sqlite, :mysql, :postgres do
5
+ let(:dragons) { Dragon.all }
6
+ let(:countries) { Country.all }
7
+
8
+ it_should_behave_like 'It Has Setup Resources'
9
+ it_should_behave_like 'An Aggregatable Class'
10
+
11
+ describe 'ignore invalid query' do
12
+ let(:dragons) { Dragon.all.all(:id => []) }
13
+
14
+ [ :size, :count ].each do |method|
15
+ describe "##{method}" do
16
+ subject { dragons.send(method) }
17
+
18
+ it { should == 0 }
19
+ end
20
+ end
21
+
22
+ describe '#min' do
23
+ subject { dragons.min(:id) }
24
+
25
+ it { should be_nil }
26
+ end
27
+
28
+ describe '#max' do
29
+ subject { dragons.max(:id) }
30
+
31
+ it { should be_nil }
32
+ end
33
+
34
+ describe '#avg' do
35
+ subject { dragons.avg(:id) }
36
+
37
+ it { should be_nil }
38
+ end
39
+
40
+ describe '#sum' do
41
+ subject { dragons.sum(:id) }
42
+
43
+ it { should be_nil }
44
+ end
45
+
46
+ describe '#aggregate' do
47
+ subject { dragons.aggregate(:id) }
48
+
49
+ it { should == [] }
50
+ end
51
+ end
52
+
53
+ context 'with collections created with Set operations' do
54
+ let(:collection) { dragons.all(:name => 'George') | dragons.all(:name => 'Puff') }
55
+
56
+ describe '#size' do
57
+ subject { collection.size }
58
+
59
+ it { should == 2 }
60
+ end
61
+
62
+ describe '#count' do
63
+ subject { collection.count }
64
+
65
+ it { should == 2 }
66
+ end
67
+
68
+ describe '#min' do
69
+ subject { collection.min(:toes_on_claw) }
70
+
71
+ it { should == 3 }
72
+ end
73
+
74
+ describe '#max' do
75
+ subject { collection.max(:toes_on_claw) }
76
+
77
+ it { should == 4 }
78
+ end
79
+
80
+ describe '#avg' do
81
+ subject { collection.avg(:toes_on_claw) }
82
+
83
+ it { should == 3.5 }
84
+ end
85
+
86
+ describe '#sum' do
87
+ subject { collection.sum(:toes_on_claw) }
88
+
89
+ it { should == 7 }
90
+ end
91
+
92
+ describe '#aggregate' do
93
+ subject { collection.aggregate(:all.count, :name.count, :toes_on_claw.min, :toes_on_claw.max, :toes_on_claw.avg, :toes_on_claw.sum)}
94
+
95
+ it { should == [ 2, 2, 3, 4, 3.5, 7 ] }
96
+ end
97
+ end
98
+
99
+ context 'with a collection limited to 1 result' do
100
+ let(:dragons) { Dragon.all(:limit => 1) }
101
+
102
+ describe '#size' do
103
+ subject { dragons.size }
104
+
105
+ it { should == 1 }
106
+ end
107
+
108
+ describe '#count' do
109
+ subject { dragons.count }
110
+
111
+ it { pending('TODO: make count apply to the limited collection. Currently limit applies after the count') { should == 1 } }
112
+ end
113
+ end
114
+
115
+ context 'with the order reversed by the grouping field' do
116
+ subject { dragons.aggregate(:birth_at, :all.count) }
117
+
118
+ let(:dragons) { Dragon.all(:order => [ :birth_at.desc ]) }
119
+
120
+ it 'displays the results in reverse order' do
121
+ should == Dragon.aggregate(:birth_at, :all.count).reverse
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe DataMapper::Model do
4
+ supported_by :sqlite, :mysql, :postgres do
5
+ let(:dragons) { Dragon }
6
+ let(:countries) { Country }
7
+
8
+ it_should_behave_like 'It Has Setup Resources'
9
+ it_should_behave_like 'An Aggregatable Class'
10
+ end
11
+ end
@@ -0,0 +1,322 @@
1
+ shared_examples_for 'It Has Setup Resources' do
2
+ before :all do
3
+ @mysql = defined?(DataMapper::Adapters::MysqlAdapter) && @adapter.kind_of?(DataMapper::Adapters::MysqlAdapter)
4
+ @postgres = defined?(DataMapper::Adapters::PostgresAdapter) && @adapter.kind_of?(DataMapper::Adapters::PostgresAdapter)
5
+
6
+ @skip = (@mysql || @postgres) && ENV['TZ'].to_s.downcase != 'utc'
7
+ end
8
+
9
+ before :all do
10
+ DataMapper.auto_migrate!
11
+
12
+ @birth_at = DateTime.now
13
+ @birth_on = Date.parse(@birth_at.to_s)
14
+ @birth_time = Time.parse(@birth_at.to_s)
15
+
16
+ @chuck = Knight.create(:name => 'Chuck')
17
+ @larry = Knight.create(:name => 'Larry')
18
+
19
+ Dragon.create(:name => 'George', :is_fire_breathing => false, :toes_on_claw => 3, :birth_at => @birth_at, :birth_on => @birth_on, :birth_time => @birth_time, :knight => @chuck)
20
+ Dragon.create(:name => 'Puff', :is_fire_breathing => true, :toes_on_claw => 4, :birth_at => @birth_at, :birth_on => @birth_on, :birth_time => @birth_time, :knight => @larry)
21
+ Dragon.create(:name => nil, :is_fire_breathing => true, :toes_on_claw => 5, :birth_at => nil, :birth_on => nil, :birth_time => nil)
22
+
23
+ gold_kilo_price = 277738.70
24
+ @gold_tonne_price = gold_kilo_price * 10000
25
+
26
+ Country.create(
27
+ :name => 'China',
28
+ :population => 1330044605,
29
+ :birth_rate => 13.71,
30
+ :gold_reserve_tonnes => 600.0,
31
+ :gold_reserve_value => 600.0 * @gold_tonne_price # => 32150000
32
+ )
33
+
34
+ Country.create(
35
+ :name => 'United States',
36
+ :population => 303824646,
37
+ :birth_rate => 14.18,
38
+ :gold_reserve_tonnes => 8133.5,
39
+ :gold_reserve_value => 8133.5 * @gold_tonne_price
40
+ )
41
+
42
+ Country.create(
43
+ :name => 'Brazil',
44
+ :population => 191908598,
45
+ :birth_rate => 16.04,
46
+ :gold_reserve_tonnes => nil # example of no stats available
47
+ )
48
+
49
+ Country.create(
50
+ :name => 'Russia',
51
+ :population => 140702094,
52
+ :birth_rate => 11.03,
53
+ :gold_reserve_tonnes => 438.2,
54
+ :gold_reserve_value => 438.2 * @gold_tonne_price
55
+ )
56
+
57
+ Country.create(
58
+ :name => 'Japan',
59
+ :population => 127288419,
60
+ :birth_rate => 7.87,
61
+ :gold_reserve_tonnes => 765.2,
62
+ :gold_reserve_value => 765.2 * @gold_tonne_price
63
+ )
64
+
65
+ Country.create(
66
+ :name => 'Mexico',
67
+ :population => 109955400,
68
+ :birth_rate => 20.04,
69
+ :gold_reserve_tonnes => nil # example of no stats available
70
+ )
71
+
72
+ Country.create(
73
+ :name => 'Germany',
74
+ :population => 82369548,
75
+ :birth_rate => 8.18,
76
+ :gold_reserve_tonnes => 3417.4,
77
+ :gold_reserve_value => 3417.4 * @gold_tonne_price
78
+ )
79
+
80
+ @approx_by = 0.0001
81
+ end
82
+ end
83
+
84
+ shared_examples_for 'An Aggregatable Class' do
85
+ describe '#size' do
86
+ it_should_behave_like 'count with no arguments'
87
+ end
88
+
89
+ describe '#count' do
90
+ it_should_behave_like 'count with no arguments'
91
+
92
+ context 'with a property name' do
93
+ it 'counts the results' do
94
+ dragons.count(:name).should == 2
95
+ end
96
+
97
+ it 'counts the results with conditions having operators' do
98
+ dragons.count(:name, :toes_on_claw.gt => 3).should == 1
99
+ end
100
+
101
+ it 'counts the results with raw conditions' do
102
+ statement = 'is_fire_breathing = ?'
103
+ dragons.count(:name, :conditions => [ statement, false ]).should == 1
104
+ dragons.count(:name, :conditions => [ statement, true ]).should == 1
105
+ end
106
+ end
107
+ end
108
+
109
+ describe '#min' do
110
+ context 'with no arguments' do
111
+ it 'raises an error' do
112
+ expect { dragons.min }.to raise_error(ArgumentError)
113
+ end
114
+ end
115
+
116
+ context 'with a property name' do
117
+ it 'provides the lowest value of an Integer property' do
118
+ dragons.min(:toes_on_claw).should == 3
119
+ countries.min(:population).should == 82369548
120
+ end
121
+
122
+ it 'provides the lowest value of a Float property' do
123
+ countries.min(:birth_rate).should be_kind_of(Float)
124
+ countries.min(:birth_rate).should >= 7.87 - @approx_by # approx match
125
+ countries.min(:birth_rate).should <= 7.87 + @approx_by # approx match
126
+ end
127
+
128
+ it 'provides the lowest value of a BigDecimal property' do
129
+ countries.min(:gold_reserve_value).should be_kind_of(BigDecimal)
130
+ countries.min(:gold_reserve_value).should == BigDecimal('1217050983400.0')
131
+ end
132
+
133
+ it 'provides the lowest value of a DateTime property' do
134
+ pending_if 'TODO: returns incorrect value until DO handles TZs properly', @skip do
135
+ dragons.min(:birth_at).should be_kind_of(DateTime)
136
+ dragons.min(:birth_at).to_s.should == @birth_at.to_s
137
+ end
138
+ end
139
+
140
+ it 'provides the lowest value of a Date property' do
141
+ dragons.min(:birth_on).should be_kind_of(Date)
142
+ dragons.min(:birth_on).should == @birth_on
143
+ end
144
+
145
+ it 'provides the lowest value of a Time property' do
146
+ dragons.min(:birth_time).should be_kind_of(Time)
147
+ dragons.min(:birth_time).to_s.should == @birth_time.to_s
148
+ end
149
+
150
+ it 'provides the lowest value when conditions provided' do
151
+ dragons.min(:toes_on_claw, :is_fire_breathing => true).should == 4
152
+ dragons.min(:toes_on_claw, :is_fire_breathing => false).should == 3
153
+ end
154
+ end
155
+ end
156
+
157
+ describe '#max' do
158
+ context 'with no arguments' do
159
+ it 'raises an error' do
160
+ expect { dragons.max }.to raise_error(ArgumentError)
161
+ end
162
+ end
163
+
164
+ context 'with a property name' do
165
+ it 'provides the highest value of an Integer property' do
166
+ dragons.max(:toes_on_claw).should == 5
167
+ countries.max(:population).should == 1330044605
168
+ end
169
+
170
+ it 'provides the highest value of a Float property' do
171
+ countries.max(:birth_rate).should be_kind_of(Float)
172
+ countries.max(:birth_rate).should >= 20.04 - @approx_by # approx match
173
+ countries.max(:birth_rate).should <= 20.04 + @approx_by # approx match
174
+ end
175
+
176
+ it 'provides the highest value of a BigDecimal property' do
177
+ countries.max(:gold_reserve_value).should == BigDecimal('22589877164500.0')
178
+ end
179
+
180
+ it 'provides the highest value of a DateTime property' do
181
+ pending_if 'TODO: returns incorrect value until DO handles TZs properly', @skip do
182
+ dragons.min(:birth_at).should be_kind_of(DateTime)
183
+ dragons.min(:birth_at).to_s.should == @birth_at.to_s
184
+ end
185
+ end
186
+
187
+ it 'provides the highest value of a Date property' do
188
+ dragons.min(:birth_on).should be_kind_of(Date)
189
+ dragons.min(:birth_on).should == @birth_on
190
+ end
191
+
192
+ it 'provides the highest value of a Time property' do
193
+ dragons.min(:birth_time).should be_kind_of(Time)
194
+ dragons.min(:birth_time).to_s.should == @birth_time.to_s
195
+ end
196
+
197
+ it 'provides the highest value when conditions provided' do
198
+ dragons.max(:toes_on_claw, :is_fire_breathing => true).should == 5
199
+ dragons.max(:toes_on_claw, :is_fire_breathing => false).should == 3
200
+ end
201
+ end
202
+ end
203
+
204
+ describe '#avg' do
205
+ context 'with no arguments' do
206
+ it 'raises an error' do
207
+ expect { dragons.avg }.to raise_error(ArgumentError)
208
+ end
209
+ end
210
+
211
+ context 'with a property name' do
212
+ it 'provides the average value of an Integer property' do
213
+ dragons.avg(:toes_on_claw).should be_kind_of(Float)
214
+ dragons.avg(:toes_on_claw).should == 4.0
215
+ end
216
+
217
+ it 'provides the average value of a Float property' do
218
+ mean_birth_rate = (13.71 + 14.18 + 16.04 + 11.03 + 7.87 + 20.04 + 8.18) / 7
219
+ countries.avg(:birth_rate).should be_kind_of(Float)
220
+ countries.avg(:birth_rate).should >= mean_birth_rate - @approx_by # approx match
221
+ countries.avg(:birth_rate).should <= mean_birth_rate + @approx_by # approx match
222
+ end
223
+
224
+ it 'provides the average value of a BigDecimal property' do
225
+ mean_gold_reserve_value = ((600.0 + 8133.50 + 438.20 + 765.20 + 3417.40) * @gold_tonne_price) / 5
226
+ countries.avg(:gold_reserve_value).should be_kind_of(BigDecimal)
227
+ countries.avg(:gold_reserve_value).should == BigDecimal(mean_gold_reserve_value.to_s)
228
+ end
229
+
230
+ it 'provides the average value when conditions provided' do
231
+ dragons.avg(:toes_on_claw, :is_fire_breathing => true).should == 4.5
232
+ dragons.avg(:toes_on_claw, :is_fire_breathing => false).should == 3
233
+ end
234
+ end
235
+ end
236
+
237
+ describe '#sum' do
238
+ context 'with no arguments' do
239
+ it 'raises an error' do
240
+ expect { dragons.sum }.to raise_error(ArgumentError)
241
+ end
242
+ end
243
+
244
+ context 'with a property name' do
245
+ it 'provides the sum of values for an Integer property' do
246
+ dragons.sum(:toes_on_claw).should == 12
247
+
248
+ total_population = 1330044605 + 303824646 + 191908598 + 140702094 +
249
+ 127288419 + 109955400 + 82369548
250
+ countries.sum(:population).should == total_population
251
+ end
252
+
253
+ it 'provides the sum of values for a Float property' do
254
+ total_tonnes = 600.0 + 8133.5 + 438.2 + 765.2 + 3417.4
255
+ countries.sum(:gold_reserve_tonnes).should be_kind_of(Float)
256
+ countries.sum(:gold_reserve_tonnes).should >= total_tonnes - @approx_by # approx match
257
+ countries.sum(:gold_reserve_tonnes).should <= total_tonnes + @approx_by # approx match
258
+ end
259
+
260
+ it 'provides the sum of values for a BigDecimal property' do
261
+ countries.sum(:gold_reserve_value).should == BigDecimal('37090059214100.0')
262
+ end
263
+
264
+ it 'provides the average value when conditions provided' do
265
+ dragons.sum(:toes_on_claw, :is_fire_breathing => true).should == 9
266
+ dragons.sum(:toes_on_claw, :is_fire_breathing => false).should == 3
267
+ end
268
+ end
269
+ end
270
+
271
+ describe '#aggregate' do
272
+ context 'with no arguments' do
273
+ it 'raises an error' do
274
+ expect { dragons.aggregate }.to raise_error(ArgumentError)
275
+ end
276
+ end
277
+
278
+ context 'with only aggregate fields specified' do
279
+ it 'provides aggregate results' do
280
+ results = dragons.aggregate(:all.count, :name.count, :toes_on_claw.min, :toes_on_claw.max, :toes_on_claw.avg, :toes_on_claw.sum)
281
+ results.should == [ 3, 2, 3, 5, 4.0, 12 ]
282
+ end
283
+ end
284
+
285
+ context 'with aggregate fields and a property to group by' do
286
+ it 'provides aggregate results' do
287
+ results = dragons.aggregate(:all.count, :name.count, :toes_on_claw.min, :toes_on_claw.max, :toes_on_claw.avg, :toes_on_claw.sum, :is_fire_breathing)
288
+ results.should == [ [ 1, 1, 3, 3, 3.0, 3, false ], [ 2, 1, 4, 5, 4.5, 9, true ] ]
289
+ end
290
+ end
291
+ end
292
+
293
+ describe 'query path issue' do
294
+ it 'does not break when a query path is specified' do
295
+ dragon = dragons.first(Dragon.knight.name => 'Chuck')
296
+ dragon.name.should == 'George'
297
+ end
298
+ end
299
+ end
300
+
301
+ shared_examples_for 'count with no arguments' do
302
+ it 'counts the results' do
303
+ dragons.count.should == 3
304
+
305
+ countries.count.should == 7
306
+ end
307
+
308
+ it 'counts the results with conditions having operators' do
309
+ dragons.count(:toes_on_claw.gt => 3).should == 2
310
+
311
+ countries.count(:birth_rate.lt => 12).should == 3
312
+ countries.count(:population.gt => 1000000000).should == 1
313
+ countries.count(:population.gt => 2000000000).should == 0
314
+ countries.count(:population.lt => 10).should == 0
315
+ end
316
+
317
+ it 'counts the results with raw conditions' do
318
+ dragon_statement = 'is_fire_breathing = ?'
319
+ dragons.count(:conditions => [ dragon_statement, false ]).should == 1
320
+ dragons.count(:conditions => [ dragon_statement, true ]).should == 2
321
+ end
322
+ end