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,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