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.
- checksums.yaml +7 -0
- data/.gitignore +35 -0
- data/.travis.yml +11 -0
- data/Gemfile +55 -0
- data/LICENSE +20 -0
- data/README.rdoc +45 -0
- data/Rakefile +4 -0
- data/VERSION +1 -0
- data/ardm-aggregates.gemspec +24 -0
- data/lib/ardm-aggregates.rb +1 -0
- data/lib/dm-aggregates.rb +60 -0
- data/lib/dm-aggregates/adapters/dm-do-adapter.rb +100 -0
- data/lib/dm-aggregates/aggregate_functions.rb +1 -0
- data/lib/dm-aggregates/collection.rb +13 -0
- data/lib/dm-aggregates/core_ext/symbol.rb +7 -0
- data/lib/dm-aggregates/functions.rb +232 -0
- data/lib/dm-aggregates/model.rb +13 -0
- data/lib/dm-aggregates/operators.rb +25 -0
- data/lib/dm-aggregates/query.rb +27 -0
- data/lib/dm-aggregates/repository.rb +13 -0
- data/lib/dm-aggregates/version.rb +5 -0
- data/spec/isolated/require_after_setup_spec.rb +21 -0
- data/spec/isolated/require_before_setup_spec.rb +21 -0
- data/spec/isolated/require_spec.rb +13 -0
- data/spec/public/collection_spec.rb +125 -0
- data/spec/public/model_spec.rb +11 -0
- data/spec/public/shared/aggregate_shared_spec.rb +322 -0
- data/spec/rcov.opts +6 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +57 -0
- data/tasks/spec.rake +38 -0
- data/tasks/yard.rake +9 -0
- data/tasks/yardstick.rake +19 -0
- metadata +123 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ecc4059ba433d1b0f1b8426199e49066d4255056
|
4
|
+
data.tar.gz: 904bc2974761000ee8984d97ca480ccc037fc6d7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 91b095a0f69ee89c481ca25f3cddc9bb5423c9c676408961f3daf09763aa6f2875f9af3e92a49e0d70d714ab86d9c3b5906b790919fab0fb4bc1bab9f89847d1
|
7
|
+
data.tar.gz: 835503ebe41fb7e7fbc3545674fa5e24c7af261736492547f3e01c612e8a6274734f625be04f3f828e7df7a48b7bdfd40fd3a616f5628486227815b362c68d71
|
data/.gitignore
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
## MAC OS
|
2
|
+
.DS_Store
|
3
|
+
|
4
|
+
## TEXTMATE
|
5
|
+
*.tmproj
|
6
|
+
tmtags
|
7
|
+
|
8
|
+
## EMACS
|
9
|
+
*~
|
10
|
+
\#*
|
11
|
+
.\#*
|
12
|
+
|
13
|
+
## VIM
|
14
|
+
*.swp
|
15
|
+
|
16
|
+
## Rubinius
|
17
|
+
*.rbc
|
18
|
+
|
19
|
+
## PROJECT::GENERAL
|
20
|
+
*.gem
|
21
|
+
coverage
|
22
|
+
rdoc
|
23
|
+
pkg
|
24
|
+
tmp
|
25
|
+
doc
|
26
|
+
log
|
27
|
+
.yardoc
|
28
|
+
measurements
|
29
|
+
|
30
|
+
## BUNDLER
|
31
|
+
.bundle
|
32
|
+
Gemfile.*
|
33
|
+
|
34
|
+
## PROJECT::SPECIFIC
|
35
|
+
spec/db/
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
source 'https://rubygems.org'
|
4
|
+
|
5
|
+
gemspec
|
6
|
+
|
7
|
+
SOURCE = ENV.fetch('SOURCE', :git).to_sym
|
8
|
+
REPO_POSTFIX = SOURCE == :path ? '' : '.git'
|
9
|
+
DATAMAPPER = SOURCE == :path ? Pathname(__FILE__).dirname.parent : 'http://github.com/ar-dm'
|
10
|
+
DM_VERSION = '~> 1.2.0'
|
11
|
+
DO_VERSION = '~> 0.10.6'
|
12
|
+
DM_DO_ADAPTERS = %w[ sqlite postgres mysql oracle sqlserver ]
|
13
|
+
CURRENT_BRANCH = ENV.fetch('GIT_BRANCH', 'master')
|
14
|
+
|
15
|
+
gem 'ardm-core', DM_VERSION,
|
16
|
+
SOURCE => "#{DATAMAPPER}/ardm-core#{REPO_POSTFIX}",
|
17
|
+
:branch => CURRENT_BRANCH
|
18
|
+
|
19
|
+
group :datamapper do
|
20
|
+
|
21
|
+
adapters = ENV['ADAPTER'] || ENV['ADAPTERS']
|
22
|
+
adapters = adapters.to_s.tr(',', ' ').split.uniq - %w[ in_memory ]
|
23
|
+
|
24
|
+
if (do_adapters = DM_DO_ADAPTERS & adapters).any?
|
25
|
+
do_options = {}
|
26
|
+
do_options[:git] = "#{DATAMAPPER}/do#{REPO_POSTFIX}" if ENV['DO_GIT'] == 'true'
|
27
|
+
|
28
|
+
gem 'data_objects', DO_VERSION, do_options.dup
|
29
|
+
|
30
|
+
do_adapters.each do |adapter|
|
31
|
+
adapter = 'sqlite3' if adapter == 'sqlite'
|
32
|
+
gem "do_#{adapter}", DO_VERSION, do_options.dup
|
33
|
+
end
|
34
|
+
|
35
|
+
gem 'ardm-do-adapter', DM_VERSION,
|
36
|
+
SOURCE => "#{DATAMAPPER}/ardm-do-adapter#{REPO_POSTFIX}",
|
37
|
+
:branch => CURRENT_BRANCH
|
38
|
+
end
|
39
|
+
|
40
|
+
adapters.each do |adapter|
|
41
|
+
gem "ardm-#{adapter}-adapter", DM_VERSION,
|
42
|
+
SOURCE => "#{DATAMAPPER}/ardm-#{adapter}-adapter#{REPO_POSTFIX}",
|
43
|
+
:branch => CURRENT_BRANCH
|
44
|
+
end
|
45
|
+
|
46
|
+
plugins = ENV['PLUGINS'] || ENV['PLUGIN']
|
47
|
+
plugins = plugins.to_s.tr(',', ' ').split.push('ardm-migrations').uniq
|
48
|
+
|
49
|
+
plugins.each do |plugin|
|
50
|
+
gem plugin, DM_VERSION,
|
51
|
+
SOURCE => "#{DATAMAPPER}/#{plugin}#{REPO_POSTFIX}",
|
52
|
+
:branch => CURRENT_BRANCH
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Foy Savas
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
dm-aggregates
|
2
|
+
=============
|
3
|
+
|
4
|
+
DataMapper plugin providing support for aggregates, functions on collections and datasets.
|
5
|
+
|
6
|
+
It provides the following functions:
|
7
|
+
|
8
|
+
== count
|
9
|
+
|
10
|
+
Count results (given the conditions)
|
11
|
+
|
12
|
+
Friend.count # returns count of all friends
|
13
|
+
Friend.count(:age.gt => 18) # returns count of all friends older then 18
|
14
|
+
Friend.count(:conditions => [ 'gender = ?', 'female' ]) # returns count of all your female friends
|
15
|
+
Friend.count(:address) # returns count of all friends with an address (NULL values are not included)
|
16
|
+
Friend.count(:address, :age.gt => 18) # returns count of all friends with an address that are older then 18
|
17
|
+
Friend.count(:address, :conditions => [ 'gender = ?', 'female' ]) # returns count of all your female friends with an address
|
18
|
+
|
19
|
+
== min
|
20
|
+
|
21
|
+
Get the lowest value of a property
|
22
|
+
|
23
|
+
Friend.min(:age) # returns the age of the youngest friend
|
24
|
+
Friend.min(:age, :conditions => [ 'gender = ?', 'female' ]) # returns the age of the youngest female friends
|
25
|
+
|
26
|
+
== max
|
27
|
+
|
28
|
+
Get the highest value of a property
|
29
|
+
|
30
|
+
Friend.max(:age) # returns the age of the oldest friend
|
31
|
+
Friend.max(:age, :conditions => [ 'gender = ?', 'female' ]) # returns the age of the oldest female friends
|
32
|
+
|
33
|
+
== avg
|
34
|
+
|
35
|
+
Get the average value of a property
|
36
|
+
|
37
|
+
Friend.avg(:age) # returns the average age of friends
|
38
|
+
Friend.avg(:age, :conditions => [ 'gender = ?', 'female' ]) # returns the average age of the female friends
|
39
|
+
|
40
|
+
== sum
|
41
|
+
|
42
|
+
Get the total value of a property
|
43
|
+
|
44
|
+
Friend.sum(:age) # returns total age of all friends
|
45
|
+
Friend.max(:age, :conditions => [ 'gender = ?', 'female' ]) # returns the total age of all female friends
|
data/Rakefile
ADDED
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.2.0
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/dm-aggregates/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.name = "ardm-aggregates"
|
6
|
+
gem.version = DataMapper::Aggregates::VERSION
|
7
|
+
|
8
|
+
gem.authors = [ 'Martin Emde', 'Emmanuel Gomez' ]
|
9
|
+
gem.email = [ 'me@martinemde.com', "emmanuel.gomez@gmail.com" ]
|
10
|
+
gem.summary = "Ardm fork of dm-aggregates"
|
11
|
+
gem.description = "DataMapper plugin providing support for aggregates on collections"
|
12
|
+
gem.homepage = "https://github.com/ar-dm/ardm-aggregates"
|
13
|
+
gem.license = 'MIT'
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split("\n")
|
16
|
+
gem.test_files = `git ls-files -- {spec}/*`.split("\n")
|
17
|
+
gem.extra_rdoc_files = %w[LICENSE README.rdoc]
|
18
|
+
gem.require_paths = [ "lib" ]
|
19
|
+
|
20
|
+
gem.add_runtime_dependency 'ardm-core', '~> 1.2'
|
21
|
+
|
22
|
+
gem.add_development_dependency 'rake', '~> 0.9'
|
23
|
+
gem.add_development_dependency 'rspec', '~> 1.3'
|
24
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'dm-aggregates'
|
@@ -0,0 +1,60 @@
|
|
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
|
+
[ :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
|
+
|
24
|
+
def self.include_aggregate_api(const_name)
|
25
|
+
require aggregate_extensions(const_name)
|
26
|
+
if Aggregates.const_defined?(const_name)
|
27
|
+
adapter = const_get(const_name)
|
28
|
+
adapter.send(:include, aggregate_module(const_name))
|
29
|
+
end
|
30
|
+
rescue LoadError
|
31
|
+
# Silently ignore the fact that no adapter extensions could be required
|
32
|
+
# This means that the adapter in use doesn't support aggregates
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.aggregate_module(const_name)
|
36
|
+
Aggregates.const_get(const_name)
|
37
|
+
end
|
38
|
+
|
39
|
+
class << self
|
40
|
+
private
|
41
|
+
# @api private
|
42
|
+
def aggregate_extensions(const_name)
|
43
|
+
name = adapter_name(const_name)
|
44
|
+
name = 'do' if name == 'dataobjects'
|
45
|
+
"dm-aggregates/adapters/dm-#{name}-adapter"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
extendable do
|
50
|
+
# @api private
|
51
|
+
def const_added(const_name)
|
52
|
+
include_aggregate_api(const_name)
|
53
|
+
super
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end # module Adapters
|
57
|
+
|
58
|
+
Aggregates.include_aggregate_api
|
59
|
+
|
60
|
+
end # module DataMapper
|
@@ -0,0 +1,100 @@
|
|
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.primitive }
|
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
|
43
|
+
|
44
|
+
def count(property, value)
|
45
|
+
value.to_i
|
46
|
+
end
|
47
|
+
|
48
|
+
def min(property, value)
|
49
|
+
property.load(value)
|
50
|
+
end
|
51
|
+
|
52
|
+
def max(property, value)
|
53
|
+
property.load(value)
|
54
|
+
end
|
55
|
+
|
56
|
+
def avg(property, value)
|
57
|
+
property.primitive == ::Integer ? value.to_f : property.load(value)
|
58
|
+
end
|
59
|
+
|
60
|
+
def sum(property, value)
|
61
|
+
property.load(value)
|
62
|
+
end
|
63
|
+
|
64
|
+
chainable do
|
65
|
+
def property_to_column_name(property, qualify)
|
66
|
+
case property
|
67
|
+
when DataMapper::Query::Operator
|
68
|
+
aggregate_field_statement(property.operator, property.target, qualify)
|
69
|
+
|
70
|
+
when Property, DataMapper::Query::Path
|
71
|
+
super
|
72
|
+
|
73
|
+
else
|
74
|
+
raise ArgumentError, "+property+ must be a DataMapper::Query::Operator, a DataMapper::Property or a Query::Path, but was a #{property.class} (#{property.inspect})"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def aggregate_field_statement(aggregate_function, property, qualify)
|
80
|
+
column_name = if aggregate_function == :count && property == :all
|
81
|
+
'*'
|
82
|
+
else
|
83
|
+
property_to_column_name(property, qualify)
|
84
|
+
end
|
85
|
+
|
86
|
+
function_name = case aggregate_function
|
87
|
+
when :count then 'COUNT'
|
88
|
+
when :min then 'MIN'
|
89
|
+
when :max then 'MAX'
|
90
|
+
when :avg then 'AVG'
|
91
|
+
when :sum then 'SUM'
|
92
|
+
else raise "Invalid aggregate function: #{aggregate_function.inspect}"
|
93
|
+
end
|
94
|
+
|
95
|
+
"#{function_name}(#{column_name})"
|
96
|
+
end
|
97
|
+
|
98
|
+
end # class DataObjectsAdapter
|
99
|
+
end # module Aggregates
|
100
|
+
end # module DataMapper
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'dm-aggregates/functions'
|
@@ -0,0 +1,232 @@
|
|
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 property [Symbol] of the property you with to count (optional)
|
27
|
+
# @param opts [Hash, Symbol] the conditions
|
28
|
+
#
|
29
|
+
# @return [Integer] return the count given the conditions
|
30
|
+
#
|
31
|
+
# @api public
|
32
|
+
def count(*args)
|
33
|
+
query = args.last.kind_of?(Hash) ? args.pop : {}
|
34
|
+
property_name = args.first
|
35
|
+
|
36
|
+
if property_name
|
37
|
+
assert_kind_of 'property', property_by_name(property_name), Property
|
38
|
+
end
|
39
|
+
|
40
|
+
aggregate(query.merge(:fields => [ property_name ? property_name.count : :all.count ])).to_i
|
41
|
+
end
|
42
|
+
|
43
|
+
# Get the lowest value of a property
|
44
|
+
#
|
45
|
+
# @example the age of the youngest friend
|
46
|
+
# Friend.min(:age)
|
47
|
+
#
|
48
|
+
# @example the age of the youngest female friend
|
49
|
+
# Friend.min(:age, :conditions => [ 'gender = ?', 'female' ])
|
50
|
+
#
|
51
|
+
# @param property [Symbol] the property you wish to get the lowest value of
|
52
|
+
# @param 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.kind_of?(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 property [Symbol] the property you wish to get the highest value of
|
75
|
+
# @param opts [Hash, Symbol] the conditions
|
76
|
+
#
|
77
|
+
# @return [Integer] return the highest value of a property given the conditions
|
78
|
+
#
|
79
|
+
# @api public
|
80
|
+
def max(*args)
|
81
|
+
query = args.last.kind_of?(Hash) ? args.pop : {}
|
82
|
+
property_name = args.first
|
83
|
+
|
84
|
+
assert_property_type property_name, ::Integer, ::Float, ::BigDecimal, ::DateTime, ::Date, ::Time
|
85
|
+
|
86
|
+
aggregate(query.merge(:fields => [ property_name.max ]))
|
87
|
+
end
|
88
|
+
|
89
|
+
# Get the average value of a property
|
90
|
+
#
|
91
|
+
# @example the average age of all friends
|
92
|
+
# Friend.avg(:age)
|
93
|
+
#
|
94
|
+
# @example the average age of all female friends
|
95
|
+
# Friend.avg(:age, :conditions => [ 'gender = ?', 'female' ])
|
96
|
+
#
|
97
|
+
# @param property [Symbol] the property you wish to get the average value of
|
98
|
+
# @param opts [Hash, Symbol] the conditions
|
99
|
+
#
|
100
|
+
# @return [Integer] return the average value of a property given the conditions
|
101
|
+
#
|
102
|
+
# @api public
|
103
|
+
def avg(*args)
|
104
|
+
query = args.last.kind_of?(Hash) ? args.pop : {}
|
105
|
+
property_name = args.first
|
106
|
+
|
107
|
+
assert_property_type property_name, ::Integer, ::Float, ::BigDecimal
|
108
|
+
|
109
|
+
aggregate(query.merge(:fields => [ property_name.avg ]))
|
110
|
+
end
|
111
|
+
|
112
|
+
# Get the total value of a property
|
113
|
+
#
|
114
|
+
# @example the total age of all friends
|
115
|
+
# Friend.sum(:age)
|
116
|
+
#
|
117
|
+
# @example the total age of all female friends
|
118
|
+
# Friend.max(:age, :conditions => [ 'gender = ?', 'female' ])
|
119
|
+
#
|
120
|
+
# @param property [Symbol] the property you wish to get the total value of
|
121
|
+
# @param opts [Hash, Symbol] the conditions
|
122
|
+
#
|
123
|
+
# @return [Integer] return the total value of a property given the conditions
|
124
|
+
#
|
125
|
+
# @api public
|
126
|
+
def sum(*args)
|
127
|
+
query = args.last.kind_of?(::Hash) ? args.pop : {}
|
128
|
+
property_name = args.first
|
129
|
+
|
130
|
+
assert_property_type property_name, ::Integer, ::Float, ::BigDecimal
|
131
|
+
|
132
|
+
aggregate(query.merge(:fields => [ property_name.sum ]))
|
133
|
+
end
|
134
|
+
|
135
|
+
# Perform aggregate queries
|
136
|
+
#
|
137
|
+
# @example the count of friends
|
138
|
+
# Friend.aggregate(:all.count)
|
139
|
+
#
|
140
|
+
# @example the minimum age, the maximum age and the total age of friends
|
141
|
+
# Friend.aggregate(:age.min, :age.max, :age.sum)
|
142
|
+
#
|
143
|
+
# @example the average age, grouped by gender
|
144
|
+
# Friend.aggregate(:age.avg, :fields => [ :gender ])
|
145
|
+
#
|
146
|
+
# @param aggregates [Symbol, ...] operators to aggregate with
|
147
|
+
# @param query [Hash] the conditions
|
148
|
+
#
|
149
|
+
# @return [Array,Numeric,DateTime,Date,Time] the results of the
|
150
|
+
# aggregate query
|
151
|
+
#
|
152
|
+
# @api public
|
153
|
+
def aggregate(*args)
|
154
|
+
query = args.last.kind_of?(Hash) ? args.pop : {}
|
155
|
+
|
156
|
+
query[:fields] ||= []
|
157
|
+
query[:fields] |= args
|
158
|
+
query[:fields].map! { |f| normalize_field(f) }
|
159
|
+
|
160
|
+
raise ArgumentError, 'query[:fields] must not be empty' if query[:fields].empty?
|
161
|
+
|
162
|
+
unless query.key?(:order)
|
163
|
+
# the current collection/model is already sorted by attributes
|
164
|
+
# and since we are projecting away some of the attributes,
|
165
|
+
# and then performing aggregate functions on the remainder,
|
166
|
+
# we need to honor the existing order, as if it were already
|
167
|
+
# materialized, and we are looping over the rows in order.
|
168
|
+
|
169
|
+
directions = direction_map
|
170
|
+
|
171
|
+
query[:order] = []
|
172
|
+
|
173
|
+
# use the current query order for each property if available
|
174
|
+
query[:fields].each do |property|
|
175
|
+
next unless property.kind_of?(Property)
|
176
|
+
query[:order] << directions.fetch(property, property)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
query = scoped_query(query)
|
181
|
+
|
182
|
+
if query.fields.any? { |p| p.kind_of?(Property) }
|
183
|
+
query.repository.aggregate(query.update(:unique => true))
|
184
|
+
else
|
185
|
+
query.repository.aggregate(query).first # only return one row
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
def assert_property_type(name, *types)
|
192
|
+
if name.nil?
|
193
|
+
raise ArgumentError, 'property name must not be nil'
|
194
|
+
end
|
195
|
+
|
196
|
+
property = property_by_name(name)
|
197
|
+
type = property.primitive
|
198
|
+
|
199
|
+
unless types.include?(type)
|
200
|
+
raise ArgumentError, "#{name} must be #{types * ' or '}, but was #{type}"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def normalize_field(field)
|
205
|
+
assert_kind_of 'field', field, DataMapper::Query::Operator, Symbol, Property
|
206
|
+
|
207
|
+
case field
|
208
|
+
when DataMapper::Query::Operator
|
209
|
+
if field.target == :all
|
210
|
+
field
|
211
|
+
else
|
212
|
+
field.class.new(property_by_name(field.target), field.operator)
|
213
|
+
end
|
214
|
+
|
215
|
+
when Symbol
|
216
|
+
property_by_name(field)
|
217
|
+
|
218
|
+
when Property
|
219
|
+
field
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def direction_map
|
224
|
+
direction_map = {}
|
225
|
+
self.query.order.each do |direction|
|
226
|
+
direction_map[direction.target] = direction
|
227
|
+
end
|
228
|
+
direction_map
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|