ardm-aggregates 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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/
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ sudo: false
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - 2.1.5
7
+ - 2.2.0
8
+ matrix:
9
+ allow_failures:
10
+ - rvm: 2.1.5
11
+ - rvm: 2.2.0
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.
@@ -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
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ FileList['tasks/**/*.rake'].each { |task| import task }
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,13 @@
1
+ module DataMapper
2
+ module Aggregates
3
+ module Collection
4
+ include Functions
5
+
6
+ private
7
+
8
+ def property_by_name(property_name)
9
+ properties[property_name]
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ require 'dm-aggregates/operators'
2
+
3
+ class Symbol
4
+
5
+ include DataMapper::Aggregates::Operators
6
+
7
+ end
@@ -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