public
Rubygem
Description: Extras for DataMapper, including bridges to DataObjects::Migrations and Merb::DataMapper
Homepage: http://datamapper.org
Clone URL: git://github.com/sam/dm-more.git
Added Model#aggregate and Collection#aggregate methods

* New aggregate method allows mixing aggregate function calls in a
  single query.  All other aggregate methods have been refactored to
  use it under the hood.  If :fields option is specified it will
  automatically GROUP BY the specified fields.  If no :order option
  specified, it will automatically ORDER BY the :fields.
* Updated documentation to match latest YARD standards (although unsure
  about the @example tags, just following convention in dm-core)
Dan Kubb (author)
Tue Jul 15 12:37:10 -0700 2008
commit  06a9599d5ed86b70441ce9fc15cc296470b42a93
tree    ee05ee97d681ab2ecdf55e22046114ce03c8b083
parent  c366fee263c578c2363b2242d61b935d8b556094
...
5
6
7
8
 
9
10
11
12
 
...
5
6
7
 
8
9
10
11
12
13
0
@@ -5,8 +5,9 @@ require 'dm-core'
0
 
0
 dir = Pathname(__FILE__).dirname.expand_path / 'dm-aggregates'
0
 
0
-require dir / 'functions'
0
+require dir / 'aggregate_functions'
0
 require dir / 'model'
0
 require dir / 'repository'
0
 require dir / 'collection'
0
 require dir / 'adapters' / 'data_objects_adapter'
0
+require dir / 'support' / 'symbol'
...
1
2
3
4
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
7
8
9
10
 
 
 
 
 
 
 
 
11
12
13
14
15
 
 
16
17
18
19
20
 
 
21
22
23
24
25
 
 
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
 
 
 
 
 
 
 
 
 
 
 
45
46
47
48
 
49
50
51
...
1
2
3
 
 
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 
 
 
24
25
26
27
28
29
30
31
32
33
 
 
 
34
35
36
37
 
 
 
38
39
40
41
 
 
 
42
43
44
45
46
47
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
50
51
52
53
54
55
56
57
58
59
60
61
62
 
63
64
65
66
0
@@ -1,51 +1,66 @@
0
 module DataMapper
0
   module Adapters
0
     class DataObjectsAdapter
0
-      def count(property, query)
0
-        query(aggregate_read_statement(:count, property, query), *query.bind_values).first
0
+      def aggregate(query)
0
+        with_reader(read_statement(query), query.bind_values) do |reader|
0
+          results = []
0
+
0
+          while(reader.next!) do
0
+            row = query.fields.zip(reader.values).map do |field,value|
0
+              if field.respond_to?(:operator)
0
+                send(field.operator, field.target, value)
0
+              else
0
+                field.typecast(value)
0
+              end
0
+            end
0
+
0
+            results << (query.fields.size > 1 ? row : row[0])
0
+          end
0
+
0
+          results
0
+        end
0
       end
0
 
0
-      def min(property, query)
0
-        min = query(aggregate_read_statement(:min, property, query), *query.bind_values).first
0
-        property.typecast(min)
0
+      private
0
+
0
+      def count(property, value)
0
+        value.to_i
0
+      end
0
+
0
+      def min(property, value)
0
+        property.typecast(value)
0
       end
0
 
0
-      def max(property, query)
0
-        max = query(aggregate_read_statement(:max, property, query), *query.bind_values).first
0
-        property.typecast(max)
0
+      def max(property, value)
0
+        property.typecast(value)
0
       end
0
 
0
-      def avg(property, query)
0
-        avg = query(aggregate_read_statement(:avg, property, query), *query.bind_values).first
0
-        property.type == Integer ? avg.to_f : property.typecast(avg)
0
+      def avg(property, value)
0
+        property.type == Integer ? value.to_f : property.typecast(value)
0
       end
0
 
0
-      def sum(property, query)
0
-        sum = query(aggregate_read_statement(:sum, property, query), *query.bind_values).first
0
-        property.typecast(sum)
0
+      def sum(property, value)
0
+        property.typecast(value)
0
       end
0
 
0
       module SQL
0
         private
0
 
0
-        def aggregate_read_statement(aggregate_function, property, query)
0
-          statement = "SELECT #{aggregate_field_statement(query.repository, aggregate_function, property, query.links.any?)}"
0
-          statement << ", #{fields_statement(query)}"                unless query.fields.empty?
0
-          statement << " FROM #{quote_table_name(query.model.storage_name(query.repository.name))}"
0
-          statement << links_statement(query)                        if query.links.any?
0
-          statement << " WHERE #{conditions_statement(query)}"       if query.conditions.any?
0
-          statement << " GROUP BY #{fields_statement(query)}"        if query.unique?
0
-          statement << " ORDER BY #{order_statement(query)}"         if query.order.any?
0
-          statement << " LIMIT #{quote_column_value(query.limit)}"   if query.limit
0
-          statement << " OFFSET #{quote_column_value(query.offset)}" if query.offset && query.offset > 0
0
-          statement
0
-        rescue => e
0
-          DataMapper.logger.error("QUERY INVALID: #{query.inspect} (#{e})")
0
-          raise e
0
+        alias original_property_to_column_name property_to_column_name
0
+
0
+        def property_to_column_name(repository, property, qualify)
0
+          case property
0
+            when Query::Operator
0
+              aggregate_field_statement(repository, property.operator, property.target, qualify)
0
+            when Property
0
+              original_property_to_column_name(repository, property, qualify)
0
+            else
0
+              raise ArgumentError, "+property+ must be a DataMapper::Query::Operator or a DataMapper::Property, but was a #{property.class} (#{property.inspect})"
0
+          end
0
         end
0
 
0
         def aggregate_field_statement(repository, aggregate_function, property, qualify)
0
-          column_name  = if aggregate_function == :count && property.nil?
0
+          column_name = if aggregate_function == :count && property == :all
0
             '*'
0
           else
0
             property_to_column_name(repository, property, qualify)
...
1
2
3
 
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 
 
26
27
28
...
1
2
 
3
4
5
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
8
9
10
11
0
@@ -1,28 +1,11 @@
0
 module DataMapper
0
   class Collection
0
-    include Aggregates
0
+    include AggregateFunctions
0
 
0
     private
0
 
0
-    def with_repository_and_property(*args, &block)
0
-      query = args.last.respond_to?(:merge) ? args.pop : {}
0
-
0
-      if query.kind_of?(Hash)
0
-        if query.has_key?(:fields) && query[:fields].any?
0
-          query[:unique] = true
0
-          query[:order] ||= query[:fields]
0
-        else
0
-          query[:fields] = []
0
-        end
0
-      end
0
-
0
-      property_name = args.first
0
-
0
-      query      = scoped_query(query)
0
-      repository = query.repository
0
-      property   = properties[property_name] if property_name
0
-
0
-      yield repository, property, query
0
+    def property_by_name(property_name)
0
+      properties[property_name]
0
     end
0
   end
0
 end
...
1
2
3
 
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 
 
26
27
28
...
1
2
 
3
4
5
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
8
9
10
11
0
@@ -1,28 +1,11 @@
0
 module DataMapper
0
   module Model
0
-    include Aggregates
0
+    include AggregateFunctions
0
 
0
     private
0
 
0
-    def with_repository_and_property(*args, &block)
0
-      query = args.last.respond_to?(:merge) ? args.pop : {}
0
-
0
-      if query.kind_of?(Hash)
0
-        if query.has_key?(:fields) && query[:fields].any?
0
-          query[:unique] = true
0
-          query[:order] ||= query[:fields]
0
-        else
0
-          query[:fields] = []
0
-        end
0
-      end
0
-
0
-      property_name = args.first
0
-
0
-      query      = scoped_query(query)
0
-      repository = query.repository
0
-      property   = properties(repository.name)[property_name] if property_name
0
-
0
-      yield repository, property, query
0
+    def property_by_name(property_name)
0
+      properties(repository.name)[property_name]
0
     end
0
   end
0
 end
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 
 
21
22
23
...
1
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
4
5
6
7
0
@@ -1,23 +1,7 @@
0
 module DataMapper
0
   class Repository
0
-    def count(property, query)
0
-      adapter.count(property, query)
0
-    end
0
-
0
-    def min(property, query)
0
-      adapter.min(property, query)
0
-    end
0
-
0
-    def max(property, query)
0
-      adapter.max(property, query)
0
-    end
0
-
0
-    def avg(property, query)
0
-      adapter.avg(property, query)
0
-    end
0
-
0
-    def sum(property, query)
0
-      adapter.sum(property, query)
0
+    def aggregate(query)
0
+      adapter.aggregate(query)
0
     end
0
   end
0
 end
...
7
8
9
10
11
12
13
14
15
 
 
 
 
 
 
 
16
17
18
19
20
21
22
23
24
25
26
27
28
 
 
29
30
31
32
33
34
35
36
 
 
 
 
 
 
 
 
 
 
37
38
39
...
144
145
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
148
149
...
174
175
176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
178
179
...
247
248
249
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
251
252
...
7
8
9
 
 
 
 
10
 
11
12
13
14
15
16
17
18
19
 
 
 
20
21
22
23
24
25
 
 
26
27
28
29
30
31
 
 
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
...
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
...
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
...
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
0
@@ -7,33 +7,40 @@ if HAS_SQLITE3 || HAS_MYSQL || HAS_POSTGRES
0
       # A simplistic example, using with an Integer property
0
       class Dragon
0
         include DataMapper::Resource
0
-        property :id, Serial
0
-        property :name, String
0
-        property :is_fire_breathing, TrueClass
0
-        property :toes_on_claw, Integer
0
 
0
-        auto_migrate!(:default)
0
+        property :id,                Serial
0
+        property :name,              String
0
+        property :is_fire_breathing, TrueClass
0
+        property :toes_on_claw,      Integer
0
+        property :birth_at,          DateTime
0
+        property :birth_on,          Date
0
+        property :birth_time,        Time
0
       end
0
 
0
-      Dragon.create(:name => 'George', :is_fire_breathing => false, :toes_on_claw => 3)
0
-      Dragon.create(:name => 'Puff',   :is_fire_breathing => true,  :toes_on_claw => 4)
0
-      Dragon.create(:name => nil,      :is_fire_breathing => true,  :toes_on_claw => 5)
0
       # A more complex example, with BigDecimal and Float properties
0
       # Statistics taken from CIA World Factbook:
0
       # https://www.cia.gov/library/publications/the-world-factbook/
0
       class Country
0
         include DataMapper::Resource
0
 
0
-        property :id,                  Integer, :serial => true
0
-        property :name,                String,  :nullable => false
0
+        property :id,                  Serial
0
+        property :name,                String,     :nullable => false
0
         property :population,          Integer
0
         property :birth_rate,          Float,      :precision => 4,  :scale => 2
0
         property :gold_reserve_tonnes, Float,      :precision => 6,  :scale => 2
0
         property :gold_reserve_value,  BigDecimal, :precision => 15, :scale => 1  # approx. value in USD
0
-
0
-        auto_migrate!(:default)
0
       end
0
 
0
+      [ Dragon, Country ].each { |m| m.auto_migrate! }
0
+
0
+      @birth_at   = DateTime.now
0
+      @birth_on   = Date.parse(@birth_at.to_s)
0
+      @birth_time = Time.parse(@birth_at.to_s)
0
+
0
+      Dragon.create(:name => 'George', :is_fire_breathing => false, :toes_on_claw => 3, :birth_at => @birth_at, :birth_on => @birth_on, :birth_time => @birth_time)
0
+      Dragon.create(:name => 'Puff',   :is_fire_breathing => true,  :toes_on_claw => 4, :birth_at => @birth_at, :birth_on => @birth_on, :birth_time => @birth_time)
0
+      Dragon.create(:name => nil,      :is_fire_breathing => true,  :toes_on_claw => 5, :birth_at => nil,       :birth_on => nil,       :birth_time => nil)
0
+
0
       gold_kilo_price  = 277738.70
0
       @gold_tonne_price = gold_kilo_price * 10000
0
 
0
@@ -144,6 +151,21 @@ if HAS_SQLITE3 || HAS_MYSQL || HAS_POSTGRES
0
             target(Country, target_type).min(:gold_reserve_value).should == BigDecimal('1217050983400.0')
0
           end
0
 
0
+          it 'should provide the lowest value of a DateTime property' do
0
+            target(Dragon, target_type).min(:birth_at).should be_kind_of(DateTime)
0
+            target(Dragon, target_type).min(:birth_at).to_s.should == @birth_at.to_s
0
+          end
0
+
0
+          it 'should provide the lowest value of a Date property' do
0
+            target(Dragon, target_type).min(:birth_on).should be_kind_of(Date)
0
+            target(Dragon, target_type).min(:birth_on).to_s.should == @birth_on.to_s
0
+          end
0
+
0
+          it 'should provide the lowest value of a Time property' do
0
+            target(Dragon, target_type).min(:birth_time).should be_kind_of(Time)
0
+            target(Dragon, target_type).min(:birth_time).to_s.should == @birth_time.to_s
0
+          end
0
+
0
           it 'should provide the lowest value when conditions provided' do
0
             target(Dragon, target_type).min(:toes_on_claw, :is_fire_breathing => true).should  == 4
0
             target(Dragon, target_type).min(:toes_on_claw, :is_fire_breathing => false).should == 3
0
@@ -174,6 +196,21 @@ if HAS_SQLITE3 || HAS_MYSQL || HAS_POSTGRES
0
             target(Country, target_type).max(:gold_reserve_value).should == BigDecimal('22589877164500.0')
0
           end
0
 
0
+          it 'should provide the highest value of a DateTime property' do
0
+            target(Dragon, target_type).min(:birth_at).should be_kind_of(DateTime)
0
+            target(Dragon, target_type).min(:birth_at).to_s.should == @birth_at.to_s
0
+          end
0
+
0
+          it 'should provide the highest value of a Date property' do
0
+            target(Dragon, target_type).min(:birth_on).should be_kind_of(Date)
0
+            target(Dragon, target_type).min(:birth_on).to_s.should == @birth_on.to_s
0
+          end
0
+
0
+          it 'should provide the highest value of a Time property' do
0
+            target(Dragon, target_type).min(:birth_time).should be_kind_of(Time)
0
+            target(Dragon, target_type).min(:birth_time).to_s.should == @birth_time.to_s
0
+          end
0
+
0
           it 'should provide the highest value when conditions provided' do
0
             target(Dragon, target_type).max(:toes_on_claw, :is_fire_breathing => true).should  == 5
0
             target(Dragon, target_type).max(:toes_on_claw, :is_fire_breathing => false).should == 3
0
@@ -247,6 +284,28 @@ if HAS_SQLITE3 || HAS_MYSQL || HAS_POSTGRES
0
           end
0
         end
0
       end
0
+
0
+      describe ".aggregate on a #{target_type}" do
0
+        describe 'with no arguments' do
0
+          it 'should raise an error' do
0
+            lambda { target(Dragon, target_type).aggregate }.should raise_error(ArgumentError)
0
+          end
0
+        end
0
+
0
+        describe 'with only aggregate fields specified' do
0
+          it 'should provide aggregate results' do
0
+            results = target(Dragon, target_type).aggregate(:all.count, :name.count, :toes_on_claw.min, :toes_on_claw.max, :toes_on_claw.avg, :toes_on_claw.sum)
0
+            results.should == [ 3, 2, 3, 5, 4.0, 12 ]
0
+          end
0
+        end
0
+
0
+        describe 'with aggregate fields and a property to group by' do
0
+          it 'should provide aggregate results' do
0
+            results = target(Dragon, target_type).aggregate(:all.count, :name.count, :toes_on_claw.min, :toes_on_claw.max, :toes_on_claw.avg, :toes_on_claw.sum, :is_fire_breathing)
0
+            results.should == [ [ 1, 1, 3, 3, 3.0, 3, false ], [ 2, 1, 4, 5, 4.5, 9, true ] ]
0
+          end
0
+        end
0
+      end
0
     end
0
   end
0
 end

Comments

dkubb Tue Jul 15 12:54:50 -0700 2008

BTW: many thanks to dbussink for the initial code contributed that made this commit possible.