sbf-dm-aggregates 1.3.0.beta
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.
- checksums.yaml +7 -0
- data/.gitignore +38 -0
- data/.rspec +5 -0
- data/.rubocop.yml +468 -0
- data/Gemfile +68 -0
- data/LICENSE +20 -0
- data/README.rdoc +45 -0
- data/Rakefile +4 -0
- data/VERSION +1 -0
- data/dm-aggregates.gemspec +20 -0
- data/lib/dm-aggregates/adapters/dm-do-adapter.rb +98 -0
- data/lib/dm-aggregates/aggregate_functions.rb +1 -0
- data/lib/dm-aggregates/collection.rb +11 -0
- data/lib/dm-aggregates/core_ext/symbol.rb +5 -0
- data/lib/dm-aggregates/functions.rb +231 -0
- data/lib/dm-aggregates/model.rb +11 -0
- data/lib/dm-aggregates/query.rb +27 -0
- data/lib/dm-aggregates/repository.rb +13 -0
- data/lib/dm-aggregates/symbol_operators.rb +25 -0
- data/lib/dm-aggregates/version.rb +5 -0
- data/lib/dm-aggregates.rb +57 -0
- data/spec/isolated/require_after_setup_spec.rb +18 -0
- data/spec/isolated/require_before_setup_spec.rb +18 -0
- data/spec/isolated/require_spec.rb +11 -0
- data/spec/public/collection_spec.rb +128 -0
- data/spec/public/model_spec.rb +11 -0
- data/spec/public/shared/aggregate_shared_spec.rb +325 -0
- data/spec/rcov.opts +6 -0
- data/spec/spec_helper.rb +55 -0
- data/tasks/release.rake +6 -0
- data/tasks/spec.rake +21 -0
- data/tasks/yard.rake +9 -0
- data/tasks/yardstick.rake +19 -0
- metadata +92 -0
@@ -0,0 +1,98 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Aggregates
|
3
|
+
module DataObjectsAdapter
|
4
|
+
extend Chainable
|
5
|
+
|
6
|
+
def aggregate(query)
|
7
|
+
fields = query.fields
|
8
|
+
types = fields.map { |p| p.respond_to?(:operator) ? String : p.dump_class }
|
9
|
+
|
10
|
+
field_size = fields.size
|
11
|
+
|
12
|
+
records = []
|
13
|
+
|
14
|
+
with_connection do |connection|
|
15
|
+
statement, bind_values = select_statement(query)
|
16
|
+
|
17
|
+
command = connection.create_command(statement)
|
18
|
+
command.set_types(types)
|
19
|
+
|
20
|
+
reader = command.execute_reader(*bind_values)
|
21
|
+
|
22
|
+
begin
|
23
|
+
while reader.next!
|
24
|
+
row = fields.zip(reader.values).map do |field, value|
|
25
|
+
if field.respond_to?(:operator)
|
26
|
+
send(field.operator, field.target, value)
|
27
|
+
else
|
28
|
+
field.load(value)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
records << ((field_size > 1) ? row : row[0])
|
33
|
+
end
|
34
|
+
ensure
|
35
|
+
reader.close
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
records
|
40
|
+
end
|
41
|
+
|
42
|
+
private def count(_property, value)
|
43
|
+
value.to_i
|
44
|
+
end
|
45
|
+
|
46
|
+
private def min(property, value)
|
47
|
+
property.load(value)
|
48
|
+
end
|
49
|
+
|
50
|
+
private def max(property, value)
|
51
|
+
property.load(value)
|
52
|
+
end
|
53
|
+
|
54
|
+
private def avg(property, value)
|
55
|
+
property.dump_class.equal?(::Integer) ? value.to_f : property.load(value)
|
56
|
+
end
|
57
|
+
|
58
|
+
private def sum(property, value)
|
59
|
+
property.load(value)
|
60
|
+
end
|
61
|
+
|
62
|
+
chainable do
|
63
|
+
def property_to_column_name(property, qualify)
|
64
|
+
case property
|
65
|
+
when DataMapper::Query::Operator
|
66
|
+
aggregate_field_statement(property.operator, property.target, qualify)
|
67
|
+
|
68
|
+
when Property, DataMapper::Query::Path
|
69
|
+
super
|
70
|
+
|
71
|
+
else
|
72
|
+
raise ArgumentError, '+property+ must be a DataMapper::Query::Operator, a DataMapper::Property or a Query::Path, but was a ' \
|
73
|
+
"#{property.class} (#{property.inspect})"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private def aggregate_field_statement(aggregate_function, property, qualify)
|
79
|
+
column_name = if aggregate_function == :count && property == :all
|
80
|
+
'*'
|
81
|
+
else
|
82
|
+
property_to_column_name(property, qualify)
|
83
|
+
end
|
84
|
+
|
85
|
+
function_name = case aggregate_function
|
86
|
+
when :count then 'COUNT'
|
87
|
+
when :min then 'MIN'
|
88
|
+
when :max then 'MAX'
|
89
|
+
when :avg then 'AVG'
|
90
|
+
when :sum then 'SUM'
|
91
|
+
else raise "Invalid aggregate function: #{aggregate_function.inspect}"
|
92
|
+
end
|
93
|
+
|
94
|
+
"#{function_name}(#{column_name})"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'dm-aggregates/functions'
|
@@ -0,0 +1,231 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Aggregates
|
3
|
+
module Functions
|
4
|
+
include DataMapper::Assertions
|
5
|
+
|
6
|
+
# Count results (given the conditions)
|
7
|
+
#
|
8
|
+
# @example the count of all friends
|
9
|
+
# Friend.count
|
10
|
+
#
|
11
|
+
# @example the count of all friends older then 18
|
12
|
+
# Friend.count(:age.gt => 18)
|
13
|
+
#
|
14
|
+
# @example the count of all your female friends
|
15
|
+
# Friend.count(:conditions => [ 'gender = ?', 'female' ])
|
16
|
+
#
|
17
|
+
# @example the count of all friends with an address (NULL values are not included)
|
18
|
+
# Friend.count(:address)
|
19
|
+
#
|
20
|
+
# @example the count of all friends with an address that are older then 18
|
21
|
+
# Friend.count(:address, :age.gt => 18)
|
22
|
+
#
|
23
|
+
# @example the count of all your female friends with an address
|
24
|
+
# Friend.count(:address, :conditions => [ 'gender = ?', 'female' ])
|
25
|
+
#
|
26
|
+
# @param args [Mixed]
|
27
|
+
# property [Symbol] of the property you with to count (optional)
|
28
|
+
# opts [Hash, Symbol] the conditions
|
29
|
+
#
|
30
|
+
# @return [Integer] return the count given the conditions
|
31
|
+
#
|
32
|
+
# @api public
|
33
|
+
def count(*args)
|
34
|
+
query = args.last.is_a?(Hash) ? args.pop : {}
|
35
|
+
property_name = args.first
|
36
|
+
|
37
|
+
assert_kind_of 'property', property_by_name(property_name), Property if property_name
|
38
|
+
|
39
|
+
aggregate(query&.merge(fields: [property_name ? property_name.count : :all.count])).to_i
|
40
|
+
end
|
41
|
+
|
42
|
+
# Get the lowest value of a property
|
43
|
+
#
|
44
|
+
# @example the age of the youngest friend
|
45
|
+
# Friend.min(:age)
|
46
|
+
#
|
47
|
+
# @example the age of the youngest female friend
|
48
|
+
# Friend.min(:age, :conditions => [ 'gender = ?', 'female' ])
|
49
|
+
#
|
50
|
+
# @param args [Mixed]
|
51
|
+
# property [Symbol] the property you wish to get the lowest value of
|
52
|
+
# opts [Hash, Symbol] the conditions
|
53
|
+
#
|
54
|
+
# @return [Integer] return the lowest value of a property given the conditions
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
def min(*args)
|
58
|
+
query = args.last.is_a?(Hash) ? args.pop : {}
|
59
|
+
property_name = args.first
|
60
|
+
|
61
|
+
assert_property_type property_name, ::Integer, ::Float, ::BigDecimal, ::DateTime, ::Date, ::Time
|
62
|
+
|
63
|
+
aggregate(query&.merge(fields: [property_name&.min]))
|
64
|
+
end
|
65
|
+
|
66
|
+
# Get the highest value of a property
|
67
|
+
#
|
68
|
+
# @example the age of the oldest friend
|
69
|
+
# Friend.max(:age)
|
70
|
+
#
|
71
|
+
# @example the age of the oldest female friend
|
72
|
+
# Friend.max(:age, :conditions => [ 'gender = ?', 'female' ])
|
73
|
+
#
|
74
|
+
# @param args [Mixed]
|
75
|
+
# property [Symbol] the property you wish to get the highest value of
|
76
|
+
# opts [Hash, Symbol] the conditions
|
77
|
+
#
|
78
|
+
# @return [Integer] return the highest value of a property given the conditions
|
79
|
+
#
|
80
|
+
# @api public
|
81
|
+
def max(*args)
|
82
|
+
query = args.last.is_a?(Hash) ? args.pop : {}
|
83
|
+
property_name = args.first
|
84
|
+
|
85
|
+
assert_property_type property_name, ::Integer, ::Float, ::BigDecimal, ::DateTime, ::Date, ::Time
|
86
|
+
|
87
|
+
aggregate(query&.merge(fields: [property_name&.max]))
|
88
|
+
end
|
89
|
+
|
90
|
+
# Get the average value of a property
|
91
|
+
#
|
92
|
+
# @example the average age of all friends
|
93
|
+
# Friend.avg(:age)
|
94
|
+
#
|
95
|
+
# @example the average age of all female friends
|
96
|
+
# Friend.avg(:age, :conditions => [ 'gender = ?', 'female' ])
|
97
|
+
#
|
98
|
+
# @param args [Mixed]
|
99
|
+
# property [Symbol] the property you wish to get the average value of
|
100
|
+
# opts [Hash, Symbol] the conditions
|
101
|
+
#
|
102
|
+
# @return [Integer] return the average value of a property given the conditions
|
103
|
+
#
|
104
|
+
# @api public
|
105
|
+
def avg(*args)
|
106
|
+
query = args.last.is_a?(Hash) ? args.pop : {}
|
107
|
+
property_name = args.first
|
108
|
+
|
109
|
+
assert_property_type property_name, ::Integer, ::Float, ::BigDecimal
|
110
|
+
|
111
|
+
aggregate(query&.merge(fields: [property_name&.avg]))
|
112
|
+
end
|
113
|
+
|
114
|
+
# Get the total value of a property
|
115
|
+
#
|
116
|
+
# @example the total age of all friends
|
117
|
+
# Friend.sum(:age)
|
118
|
+
#
|
119
|
+
# @example the total age of all female friends
|
120
|
+
# Friend.max(:age, :conditions => [ 'gender = ?', 'female' ])
|
121
|
+
#
|
122
|
+
# @param args [Mixed]
|
123
|
+
# property [Symbol] the property you wish to get the total value of
|
124
|
+
# opts [Hash, Symbol] the conditions
|
125
|
+
#
|
126
|
+
# @return [Integer] return the total value of a property given the conditions
|
127
|
+
#
|
128
|
+
# @api public
|
129
|
+
def sum(*args)
|
130
|
+
query = args.last.is_a?(::Hash) ? args.pop : {}
|
131
|
+
property_name = args.first
|
132
|
+
|
133
|
+
assert_property_type property_name, ::Integer, ::Float, ::BigDecimal
|
134
|
+
|
135
|
+
aggregate(query.merge(fields: [property_name.sum]))
|
136
|
+
end
|
137
|
+
|
138
|
+
# Perform aggregate queries
|
139
|
+
#
|
140
|
+
# @example the count of friends
|
141
|
+
# Friend.aggregate(:all.count)
|
142
|
+
#
|
143
|
+
# @example the minimum age, the maximum age and the total age of friends
|
144
|
+
# Friend.aggregate(:age.min, :age.max, :age.sum)
|
145
|
+
#
|
146
|
+
# @example the average age, grouped by gender
|
147
|
+
# Friend.aggregate(:age.avg, :fields => [ :gender ])
|
148
|
+
#
|
149
|
+
# @param args [Mixed]
|
150
|
+
# aggregates [Symbol, ...] operators to aggregate with
|
151
|
+
# query [Hash] the conditions
|
152
|
+
#
|
153
|
+
# @return [Array,Numeric,DateTime,Date,Time] the results of the
|
154
|
+
# aggregate query
|
155
|
+
#
|
156
|
+
# @api public
|
157
|
+
def aggregate(*args)
|
158
|
+
query = args.last.is_a?(Hash) ? args.pop : {}
|
159
|
+
|
160
|
+
query[:fields] ||= []
|
161
|
+
query[:fields] |= args
|
162
|
+
query[:fields].map! { |f| normalize_field(f) }
|
163
|
+
|
164
|
+
raise ArgumentError, 'query[:fields] must not be empty' if query[:fields].empty?
|
165
|
+
|
166
|
+
unless query&.key?(:order)
|
167
|
+
# the current collection/model is already sorted by attributes
|
168
|
+
# and since we are projecting away some of the attributes,
|
169
|
+
# and then performing aggregate functions on the remainder,
|
170
|
+
# we need to honor the existing order, as if it were already
|
171
|
+
# materialized, and we are looping over the rows in order.
|
172
|
+
|
173
|
+
directions = direction_map
|
174
|
+
|
175
|
+
query[:order] = []
|
176
|
+
|
177
|
+
# use the current query order for each property if available
|
178
|
+
query[:fields].each do |property|
|
179
|
+
next unless property.is_a?(Property)
|
180
|
+
|
181
|
+
query[:order] << directions.fetch(property, property)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
query = scoped_query(query)
|
186
|
+
|
187
|
+
if query.fields.any? { |p| p.is_a?(Property) }
|
188
|
+
query.repository.aggregate(query.update(unique: true))
|
189
|
+
else
|
190
|
+
query.repository.aggregate(query).first # only return one row
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
private def assert_property_type(name, *types)
|
195
|
+
raise ArgumentError, 'property name must not be nil' if name.nil?
|
196
|
+
|
197
|
+
property = property_by_name(name)
|
198
|
+
type = property.dump_class
|
199
|
+
|
200
|
+
raise ArgumentError, "#{name} must be #{types * ' or '}, but was #{type}" unless types.include?(type)
|
201
|
+
end
|
202
|
+
|
203
|
+
private def normalize_field(field)
|
204
|
+
assert_kind_of 'field', field, DataMapper::Query::Operator, Symbol, Property
|
205
|
+
|
206
|
+
case field
|
207
|
+
when DataMapper::Query::Operator
|
208
|
+
if field.target == :all
|
209
|
+
field
|
210
|
+
else
|
211
|
+
field.class.new(property_by_name(field.target), field.operator)
|
212
|
+
end
|
213
|
+
|
214
|
+
when Symbol
|
215
|
+
property_by_name(field)
|
216
|
+
|
217
|
+
when Property
|
218
|
+
field
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
private def direction_map
|
223
|
+
direction_map = {}
|
224
|
+
query.order.each do |direction|
|
225
|
+
direction_map[direction.target] = direction
|
226
|
+
end
|
227
|
+
direction_map
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
@@ -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.is_a?(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,25 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Aggregates
|
3
|
+
module SymbolOperators
|
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
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'dm-core'
|
2
|
+
|
3
|
+
require 'dm-aggregates/aggregate_functions'
|
4
|
+
require 'dm-aggregates/collection'
|
5
|
+
require 'dm-aggregates/core_ext/symbol'
|
6
|
+
require 'dm-aggregates/model'
|
7
|
+
require 'dm-aggregates/query'
|
8
|
+
require 'dm-aggregates/repository'
|
9
|
+
|
10
|
+
module DataMapper
|
11
|
+
module Aggregates
|
12
|
+
def self.include_aggregate_api
|
13
|
+
%i(Repository Model Collection Query).each do |name|
|
14
|
+
DataMapper.const_get(name).send(:include, const_get(name))
|
15
|
+
end
|
16
|
+
Adapters::AbstractAdapter.descendants.each do |adapter_class|
|
17
|
+
Adapters.include_aggregate_api(DataMapper::Inflector.demodulize(adapter_class.name))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module Adapters
|
23
|
+
def self.include_aggregate_api(const_name)
|
24
|
+
require aggregate_extensions(const_name)
|
25
|
+
if Aggregates.const_defined?(const_name)
|
26
|
+
adapter = const_get(const_name)
|
27
|
+
adapter.send(:include, aggregate_module(const_name))
|
28
|
+
end
|
29
|
+
rescue LoadError
|
30
|
+
# Silently ignore the fact that no adapter extensions could be required
|
31
|
+
# This means that the adapter in use doesn't support aggregates
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.aggregate_module(const_name)
|
35
|
+
Aggregates.const_get(const_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
class << self
|
39
|
+
# @api private
|
40
|
+
private def aggregate_extensions(const_name)
|
41
|
+
name = adapter_name(const_name)
|
42
|
+
name = 'do' if name == 'dataobjects'
|
43
|
+
"dm-aggregates/adapters/dm-#{name}-adapter"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
extendable do
|
48
|
+
# @api private
|
49
|
+
def const_added(const_name)
|
50
|
+
include_aggregate_api(const_name)
|
51
|
+
super
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
Aggregates.include_aggregate_api
|
57
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
|
3
|
+
require_relative '../isolated/require_spec'
|
4
|
+
require 'dm-core/spec/setup'
|
5
|
+
|
6
|
+
# To really test this behavior, this spec needs to be run in isolation and not
|
7
|
+
# as part of the typical rake spec run, which requires dm-aggregates upfront
|
8
|
+
|
9
|
+
if %w(postgres mysql sqlite oracle sqlserver).include?(ENV['ADAPTER'])
|
10
|
+
describe "require 'dm-aggregates after calling DataMapper.setup" do
|
11
|
+
before(:all) do
|
12
|
+
@adapter = DataMapper::Spec.adapter
|
13
|
+
require 'dm-aggregates'
|
14
|
+
end
|
15
|
+
|
16
|
+
it_behaves_like "require 'dm-aggregates'"
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
|
3
|
+
require_relative 'require_spec'
|
4
|
+
require 'dm-core/spec/setup'
|
5
|
+
|
6
|
+
# To really test this behavior, this spec needs to be run in isolation and not
|
7
|
+
# as part of the typical rake spec run, which requires dm-aggregates upfront
|
8
|
+
|
9
|
+
if %w(postgres mysql sqlite oracle sqlserver).include?(ENV['ADAPTER'])
|
10
|
+
describe "require 'dm-aggregates' before calling DataMapper.setup" do
|
11
|
+
before(:all) do
|
12
|
+
require 'dm-aggregates'
|
13
|
+
@adapter = DataMapper::Spec.adapter
|
14
|
+
end
|
15
|
+
|
16
|
+
it_behaves_like "require 'dm-aggregates'"
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
shared_examples "require 'dm-aggregates'" do
|
2
|
+
%w(Repository Model Collection Query).each do |name|
|
3
|
+
it "includes the aggregate api in DataMapper::#{name}" do
|
4
|
+
expect(DataMapper.const_get(name) < DataMapper::Aggregates.const_get(name)).to be(true)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'includes the aggregate api into the adapter' do
|
9
|
+
expect(@adapter.respond_to?(:aggregate)).to be(true)
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require_relative '../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_behaves_like 'It Has Setup Resources'
|
9
|
+
it_behaves_like 'An Aggregatable Class'
|
10
|
+
|
11
|
+
describe 'ignore invalid query' do
|
12
|
+
let(:dragons) { Dragon.all.all(id: []) }
|
13
|
+
|
14
|
+
%i(size count).each do |method|
|
15
|
+
describe "##{method}" do
|
16
|
+
subject { dragons.send(method) }
|
17
|
+
|
18
|
+
it { is_expected.to eq 0 }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '#min' do
|
23
|
+
subject { dragons.min(:id) }
|
24
|
+
|
25
|
+
it { is_expected.to be_nil }
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#max' do
|
29
|
+
subject { dragons.max(:id) }
|
30
|
+
|
31
|
+
it { is_expected.to be_nil }
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#avg' do
|
35
|
+
subject { dragons.avg(:id) }
|
36
|
+
|
37
|
+
it { is_expected.to be_nil }
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#sum' do
|
41
|
+
subject { dragons.sum(:id) }
|
42
|
+
|
43
|
+
it { is_expected.to be_nil }
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '#aggregate' do
|
47
|
+
subject { dragons.aggregate(:id) }
|
48
|
+
|
49
|
+
it { is_expected.to eq [] }
|
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 { is_expected.to eq 2 }
|
60
|
+
end
|
61
|
+
|
62
|
+
describe '#count' do
|
63
|
+
subject { collection.count }
|
64
|
+
|
65
|
+
it { is_expected.to eq 2 }
|
66
|
+
end
|
67
|
+
|
68
|
+
describe '#min' do
|
69
|
+
subject { collection.min(:toes_on_claw) }
|
70
|
+
|
71
|
+
it { is_expected.to eq 3 }
|
72
|
+
end
|
73
|
+
|
74
|
+
describe '#max' do
|
75
|
+
subject { collection.max(:toes_on_claw) }
|
76
|
+
|
77
|
+
it { is_expected.to eq 4 }
|
78
|
+
end
|
79
|
+
|
80
|
+
describe '#avg' do
|
81
|
+
subject { collection.avg(:toes_on_claw) }
|
82
|
+
|
83
|
+
it { is_expected.to eq 3.5 }
|
84
|
+
end
|
85
|
+
|
86
|
+
describe '#sum' do
|
87
|
+
subject { collection.sum(:toes_on_claw) }
|
88
|
+
|
89
|
+
it { is_expected.to eq 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 { is_expected.to eq [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 { is_expected.to eq 1 }
|
106
|
+
end
|
107
|
+
|
108
|
+
describe '#count' do
|
109
|
+
subject { dragons.count }
|
110
|
+
|
111
|
+
it {
|
112
|
+
pending 'TODO: make count apply to the limited collection. Currently limit applies after the count'
|
113
|
+
is_expected.to eq 1
|
114
|
+
}
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
context 'with the order reversed by the grouping field' do
|
119
|
+
subject { dragons.aggregate(:birth_at, :all.count) }
|
120
|
+
|
121
|
+
let(:dragons) { Dragon.all(order: [:birth_at.desc]) }
|
122
|
+
|
123
|
+
it 'displays the results in reverse order' do
|
124
|
+
is_expected.to eq Dragon.aggregate(:birth_at, :all.count).reverse
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require_relative '../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_behaves_like 'It Has Setup Resources'
|
9
|
+
it_behaves_like 'An Aggregatable Class'
|
10
|
+
end
|
11
|
+
end
|