sam / dm-more

Extras for DataMapper, including bridges to DataObjects::Migrations and Merb::DataMapper

This URL has Read+Write access

dm-more / dm-aggregates / lib / dm-aggregates / aggregate_functions.rb
100644 201 lines (175 sloc) 6.341 kb
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
module DataMapper
  module AggregateFunctions
    # Count results (given the conditions)
    #
    # @example the count of all friends
    # Friend.count
    #
    # @example the count of all friends older then 18
    # Friend.count(:age.gt => 18)
    #
    # @example the count of all your female friends
    # Friend.count(:conditions => [ 'gender = ?', 'female' ])
    #
    # @example the count of all friends with an address (NULL values are not included)
    # Friend.count(:address)
    #
    # @example the count of all friends with an address that are older then 18
    # Friend.count(:address, :age.gt => 18)
    #
    # @example the count of all your female friends with an address
    # Friend.count(:address, :conditions => [ 'gender = ?', 'female' ])
    #
    # @param property [Symbol] of the property you with to count (optional)
    # @param opts [Hash, Symbol] the conditions
    #
    # @return [Integer] return the count given the conditions
    #
    # @api public
    def count(*args)
      query = args.last.kind_of?(Hash) ? args.pop : {}
      property_name = args.first
 
      if property_name
        assert_kind_of 'property', property_by_name(property_name), Property
      end
 
      aggregate(query.merge(:fields => [ property_name ? property_name.count : :all.count ]))
    end
 
    # Get the lowest value of a property
    #
    # @example the age of the youngest friend
    # Friend.min(:age)
    #
    # @example the age of the youngest female friend
    # Friend.min(:age, :conditions => [ 'gender = ?', 'female' ])
    #
    # @param property [Symbol] the property you wish to get the lowest value of
    # @param opts [Hash, Symbol] the conditions
    #
    # @return [Integer] return the lowest value of a property given the conditions
    #
    # @api public
    def min(*args)
      query = args.last.kind_of?(Hash) ? args.pop : {}
      property_name = args.first
 
      assert_property_type property_name, Integer, Float, BigDecimal, DateTime, Date, Time
 
      aggregate(query.merge(:fields => [ property_name.min ]))
    end
 
    # Get the highest value of a property
    #
    # @example the age of the oldest friend
    # Friend.max(:age)
    #
    # @example the age of the oldest female friend
    # Friend.max(:age, :conditions => [ 'gender = ?', 'female' ])
    #
    # @param property [Symbol] the property you wish to get the highest value of
    # @param opts [Hash, Symbol] the conditions
    #
    # @return [Integer] return the highest value of a property given the conditions
    #
    # @api public
    def max(*args)
      query = args.last.kind_of?(Hash) ? args.pop : {}
      property_name = args.first
 
      assert_property_type property_name, Integer, Float, BigDecimal, DateTime, Date, Time
 
      aggregate(query.merge(:fields => [ property_name.max ]))
    end
 
    # Get the average value of a property
    #
    # @example the average age of all friends
    # Friend.avg(:age)
    #
    # @example the average age of all female friends
    # Friend.avg(:age, :conditions => [ 'gender = ?', 'female' ])
    #
    # @param property [Symbol] the property you wish to get the average value of
    # @param opts [Hash, Symbol] the conditions
    #
    # @return [Integer] return the average value of a property given the conditions
    #
    # @api public
    def avg(*args)
      query = args.last.kind_of?(Hash) ? args.pop : {}
      property_name = args.first
 
      assert_property_type property_name, Integer, Float, BigDecimal
 
      aggregate(query.merge(:fields => [ property_name.avg ]))
    end
 
    # Get the total value of a property
    #
    # @example the total age of all friends
    # Friend.sum(:age)
    #
    # @example the total age of all female friends
    # Friend.max(:age, :conditions => [ 'gender = ?', 'female' ])
    #
    # @param property [Symbol] the property you wish to get the total value of
    # @param opts [Hash, Symbol] the conditions
    #
    # @return [Integer] return the total value of a property given the conditions
    #
    # @api public
    def sum(*args)
      query = args.last.kind_of?(Hash) ? args.pop : {}
      property_name = args.first
 
      assert_property_type property_name, Integer, Float, BigDecimal
 
      aggregate(query.merge(:fields => [ property_name.sum ]))
    end
 
    # Perform aggregate queries
    #
    # @example the count of friends
    # Friend.aggregate(:all.count)
    #
    # @example the minimum age, the maximum age and the total age of friends
    # Friend.aggregate(:age.min, :age.max, :age.sum)
    #
    # @example the average age, grouped by gender
    # Friend.aggregate(:age.avg, :fields => [ :gender ])
    #
    # @param aggregates [Symbol, ...] operators to aggregate with
    # @params query [Hash] the conditions
    #
    # @return [Array,Numeric,DateTime,Date,Time] the results of the
    # aggregate query
    #
    # @api public
    def aggregate(*args)
      query = args.last.kind_of?(Hash) ? args.pop : {}
 
      query[:fields] ||= []
      query[:fields] |= args
      query[:fields].map! { |f| normalize_field(f) }
      query[:order] ||= query[:fields].select { |p| p.kind_of?(Property) }
 
      raise ArgumentError, 'query[:fields] must not be empty' if query[:fields].empty?
 
      query = scoped_query(query)
 
      if query.fields.any? { |p| p.kind_of?(Property) }
        query.repository.aggregate(query.update(:unique => true))
      else
        query.repository.aggregate(query).first # only return one row
      end
    end
 
    private
 
    def assert_property_type(name, *types)
      if name.nil?
        raise ArgumentError, 'property name must not be nil'
      end
 
      type = property_by_name(name).type
 
      unless types.include?(type)
        raise ArgumentError, "#{name} must be #{types * ' or '}, but was #{type}"
      end
    end
 
    def normalize_field(field)
      assert_kind_of 'field', field, Query::Operator, Symbol, Property
 
      case field
        when Query::Operator
          if field.target == :all
            field
          else
            field.class.new(property_by_name(field.target), field.operator)
          end
        when Symbol
          property_by_name(field)
        when Property
          field
      end
    end
  end
end