cubicle 0.1.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,10 @@
1
+ module Cubicle
2
+ class Aggregation
3
+ include Cubicle
4
+ def initialize(source_collection,&block)
5
+ transient!
6
+ source_collection_name source_collection
7
+ instance_eval(&block) if block_given?
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ module Cubicle
2
+ class CalculatedMeasure < Measure
3
+
4
+ def initialize(*args)
5
+ opts = args.extract_options!
6
+ opts[:aggregation_method] = :calculation
7
+ args << opts
8
+ super(*args)
9
+ end
10
+ #calculated members to not participate in the map/reduce
11
+ #cycle. They are a finalization-time only
12
+ #concept.
13
+ def to_js_keys
14
+ []
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,84 @@
1
+ module Cubicle
2
+ class Data < Array
3
+
4
+ attr_reader :dimension_names, :measure_names, :total_count
5
+
6
+ def initialize(query,query_results,total_count = nil)
7
+ @dimension_names = query.dimensions.map{|d|d.name}
8
+ @measure_names = query.measures.map{|m|m.name}
9
+ @time_dimension_name = query.time_dimension.name if query.respond_to?(:time_dimension) && query.time_dimension
10
+ @time_period = query.time_period if query.respond_to?(:time_period)
11
+ @time_range = query.time_range if query.respond_to?(:time_range)
12
+ extract_data(query_results)
13
+ @total_count = total_count if total_count
14
+ end
15
+
16
+ def hierarchize(*args)
17
+ args = [@time_dimension_name || @dimension_names].flatten if args.blank?
18
+ extract_dimensions args, self
19
+ end
20
+ alias hierarchize_by hierarchize
21
+ alias by hierarchize
22
+
23
+ def records_per_page=(records_per_page)
24
+ @records_per_page=records_per_page
25
+ end
26
+
27
+ def total_pages
28
+ if (!defined?(@total_count))
29
+ raise "Cannot find the total number of pages without setting the total count"
30
+ end
31
+
32
+ if (!defined?(@records_per_page))
33
+ raise "Cannot find the total number of pages without setting the number of records per page"
34
+ end
35
+
36
+ (@total_count.to_f / @records_per_page.to_f).ceil
37
+ end
38
+
39
+ private
40
+
41
+ def extract_dimensions(dimension_names, data)
42
+ data, dimension_names = data.dup, dimension_names.dup
43
+
44
+ return data.map{|measures|Cubicle::DataLevel.new(:measures,measures)} if dimension_names.blank?
45
+
46
+ dim_name = dimension_names.shift
47
+
48
+ result = Cubicle::DataLevel.new(dim_name)
49
+ data.each do |tuple|
50
+ member_name = tuple.delete(dim_name.to_s) || "Unknown"
51
+ result[member_name] << tuple
52
+ end
53
+
54
+ result.each do |key,value|
55
+ result[key] = extract_dimensions(dimension_names,value)
56
+ end
57
+
58
+ expand_time_dimension_if_required(result)
59
+
60
+ result
61
+ end
62
+
63
+ def extract_data(data)
64
+ data.each do |result|
65
+ new = result.dup
66
+ self << new.delete("_id").merge(new.delete("value"))
67
+ end
68
+ end
69
+
70
+ def expand_time_dimension_if_required(data_level)
71
+ return unless data_level.leaf_level? && @time_dimension_name && @time_dimension_name.to_s == data_level.name.to_s &&
72
+ @time_range && @time_period
73
+
74
+ @time_range.by!(@time_period)
75
+
76
+ @time_range.each do |date|
77
+ formatted_date = date.to_cubicle(@time_period)
78
+ data_level[formatted_date] = [Cubicle::DataLevel.new(:measures,{})] unless data_level.include?(formatted_date)
79
+ end
80
+ data_level.keys.sort!
81
+ end
82
+ end
83
+ end
84
+
@@ -0,0 +1,60 @@
1
+ module Cubicle
2
+ class DataLevel < OrderedHash
3
+
4
+ def initialize(name = "Unknown Level", initial_data = {})
5
+ @name = name
6
+ merge!(initial_data.stringify_keys)
7
+ end
8
+
9
+ attr_reader :name
10
+ attr_accessor :missing_member_default
11
+
12
+ alias member_names keys
13
+
14
+ def [](key)
15
+ key = key.to_s
16
+ self[key] = [] unless self.keys.include?(key)
17
+ super(key)
18
+ end
19
+
20
+ def []=(key,val)
21
+ super(key.to_s,val)
22
+ end
23
+
24
+ def include?(key)
25
+ super(key.to_s)
26
+ end
27
+
28
+ def flatten(member_name = nil, opts={}, &block)
29
+
30
+ default_val = opts[:default] || @missing_member_default || 0
31
+
32
+ self.inject([]) do |output, (key, data)|
33
+ data.inject(output) do |flattened, value|
34
+ value.missing_member_default = default_val if value.respond_to?(:missing_member_default)
35
+
36
+ if block_given?
37
+ flat_val = block.arity == 1 ? (yield value) : (value.instance_eval(&block))
38
+ end
39
+ flat_val ||= value[member_name] if member_name && value.include?(member_name)
40
+ flat_val ||= default_val
41
+ flattened << flat_val
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ def leaf_level?
48
+ return self.length < 1 ||
49
+ !self[self.keys[0]].is_a?(Cubicle::DataLevel)
50
+ end
51
+
52
+ def method_missing(sym,*args,&block)
53
+ return self[sym.to_s[0..-2]] = args[0] if sym.to_s =~ /.*=$/
54
+ return self[sym] if self.keys.include?(sym.to_s)
55
+ missing_member_default
56
+ end
57
+
58
+
59
+ end
60
+ end
@@ -0,0 +1,33 @@
1
+ module Cubicle
2
+ module DateTime
3
+ def self.db_time_format
4
+ @time_format ||= :iso8601 #or :native || :time || anything not :iso8601
5
+ end
6
+
7
+ def self.db_time_format=(time_format)
8
+ raise "db_time_format must be :iso8601 or :native" unless [:iso8601,:native].include?(time_format)
9
+ @time_format=time_format
10
+ end
11
+
12
+ def self.iso8601?
13
+ self.db_time_format == :iso8601
14
+ end
15
+
16
+ def iso8601?
17
+ Cubicle::DateTime.iso8601?
18
+ end
19
+
20
+ def to_cubicle(period = :date)
21
+ case period
22
+ when :year, :years then iso8601? ? self.strftime('%Y') : beginning_of_year
23
+ when :quarter, :quarters then iso8601? ? "#{db_year}-Q#{(month+2) / 3}" : beginning_of_quarter
24
+ when :month, :months then iso8601? ? self.strftime('%Y-%m') : beginning_of_month
25
+ else iso8601? ? self.strftime('%Y-%m-%d') : self
26
+ end
27
+ end
28
+
29
+ def beginning_of(period)
30
+ self.send "beginning_of_#{period.to_s.singularize}"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,4 @@
1
+ module Cubicle
2
+ class Dimension < Member
3
+ end
4
+ end
@@ -0,0 +1,16 @@
1
+ module Cubicle
2
+ class Measure < Member
3
+
4
+ def initialize(*args)
5
+ super
6
+ @aggregation_method = self.options.delete(:aggregation_method) || :count
7
+ end
8
+
9
+ attr_accessor :aggregation_method #can be :sum, :average, :count
10
+
11
+ def to_js_value
12
+ return super unless aggregation_method == :count
13
+ "((#{super}) ? 1 : 0)"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,53 @@
1
+ module Cubicle
2
+ class Member
3
+
4
+ attr_accessor :name,
5
+ :expression,
6
+ :expression_type, #can be :field_name, :javascript
7
+ :options,
8
+ :alias_list
9
+
10
+ def initialize(*args)
11
+ opts = args.extract_options!
12
+ @name = args.shift.to_sym
13
+
14
+ self.options = (opts || {}).symbolize_keys
15
+
16
+ if (@expression = self.options.delete(:field_name))
17
+ @expression_type = :field_name
18
+ elsif (@expression = self.options.delete(:expression))
19
+ @expression_type = :javascript
20
+ else
21
+ @expression = @name
22
+ @expression_type = :field_name
23
+ end
24
+
25
+ member_alias = self.options[:alias]
26
+ if (member_alias)
27
+ member_alias = [member_alias] unless member_alias.is_a?(Array)
28
+ @alias_list = member_alias.map{|a|a.to_s}
29
+ end
30
+
31
+ end
32
+
33
+ def matches(member_name)
34
+ return name.to_s == member_name.to_s || (@alias_list||=[]).include?(member_name.to_s)
35
+ end
36
+
37
+ def included_in?(list_of_member_names)
38
+ list_of_member_names.each do |member_name|
39
+ return true if matches(member_name)
40
+ end
41
+ false
42
+ end
43
+
44
+ def to_js_keys
45
+ ["#{name}:#{to_js_value}"]
46
+ end
47
+
48
+ def to_js_value
49
+ prefix, suffix = expression_type == :field_name ? ['this.',''] : ['(',')']
50
+ "#{prefix}#{expression}#{suffix}"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,11 @@
1
+ module Cubicle
2
+ class MemberList < Array
3
+ def [](member_name)
4
+ if (member_name.is_a?(Integer))
5
+ return super(member_name)
6
+ end
7
+ self.find{|m|m.matches(member_name)}
8
+ end
9
+ #Code here
10
+ end
11
+ end
@@ -0,0 +1,102 @@
1
+ module Cubicle
2
+ class MongoEnvironment
3
+
4
+ # @api public
5
+ def self.connection
6
+ @@connection ||= Mongo::Connection.new
7
+ end
8
+
9
+ # @api public
10
+ def self.connection=(new_connection)
11
+ @@connection = new_connection
12
+ end
13
+
14
+ # @api public
15
+ def self.logger
16
+ connection.logger
17
+ end
18
+
19
+ # @api public
20
+ def self.database=(name)
21
+ @@database = nil
22
+ @@database_name = name
23
+ end
24
+
25
+ # @api public
26
+ def self.database
27
+ if @@database_name.blank?
28
+ raise 'You forgot to set the default database name: MongoMapper.database = "foobar"'
29
+ end
30
+
31
+ @@database ||= connection.db(@@database_name)
32
+ end
33
+
34
+ def self.config=(hash)
35
+ @@config = hash
36
+ end
37
+
38
+ def self.config
39
+ raise 'Set config before connecting. Cubicle.mongo.config = {...}' unless defined?(@@config)
40
+ @@config
41
+ end
42
+
43
+ # @api private
44
+ def self.config_for_environment(environment)
45
+ env = config[environment]
46
+ return env if env['uri'].blank?
47
+
48
+ uri = URI.parse(env['uri'])
49
+ raise InvalidScheme.new('must be mongodb') unless uri.scheme == 'mongodb'
50
+ {
51
+ 'host' => uri.host,
52
+ 'port' => uri.port,
53
+ 'database' => uri.path.gsub(/^\//, ''),
54
+ 'username' => uri.user,
55
+ 'password' => uri.password,
56
+ }
57
+ end
58
+
59
+ def self.connect(environment, options={})
60
+ raise 'Set config before connecting. Cubicle.mongo.config = {...}' if config.blank?
61
+ env = config_for_environment(environment)
62
+ self.connection = Mongo::Connection.new(env['host'], env['port'], options)
63
+ self.database = env['database']
64
+ self.database.authenticate(env['username'], env['password']) if env['username'] && env['password']
65
+ end
66
+
67
+ def self.setup(config, environment, options={})
68
+ using_passenger = options.delete(:passenger)
69
+ handle_passenger_forking if using_passenger
70
+ self.config = config
71
+ connect(environment, options)
72
+ end
73
+
74
+ def self.handle_passenger_forking
75
+ if defined?(PhusionPassenger)
76
+ PhusionPassenger.on_event(:starting_worker_process) do |forked|
77
+ connection.connect_to_master if forked
78
+ end
79
+ end
80
+ end
81
+
82
+ # @api private
83
+ def self.use_time_zone?
84
+ Time.respond_to?(:zone) && Time.zone ? true : false
85
+ end
86
+
87
+ # @api private
88
+ def self.time_class
89
+ use_time_zone? ? Time.zone : Time
90
+ end
91
+
92
+ # @api private
93
+ def self.normalize_object_id(value)
94
+ value.is_a?(String) ? Mongo::ObjectID.from_string(value) : value
95
+ end
96
+ end
97
+ end
98
+ #This class represents MongoDB. It is lifted line for line from MongoMapper
99
+ #http://github.com/jnunemaker/mongomapper/blob/master/lib/mongo_mapper.rb
100
+ #Actually, if the MongoMapper gem is loaded, Cubicle will simply use it for
101
+ #providing the MongoEnvironment. However, if MongoMapper isn't loaded,
102
+ #this stuff is still required, so why reinvent the wheel?
@@ -0,0 +1,17 @@
1
+ module Cubicle
2
+ module MongoMapper
3
+ module AggregatePlugin
4
+ module ClassMethods
5
+ def aggregate(&block)
6
+ return Cubicle::Aggregation.new(self.collection_name,&block)
7
+ end
8
+ end
9
+
10
+ def self.included(model)
11
+ model.plugin AggregatePlugin
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ MongoMapper::Document.append_inclusions(Cubicle::MongoMapper::AggregatePlugin)
@@ -0,0 +1,315 @@
1
+ module Cubicle
2
+ class Query
3
+
4
+ attr_reader :time_period, :transient
5
+ attr_accessor :source_collection_name
6
+ def initialize(cubicle)
7
+ @cubicle = cubicle
8
+
9
+ @dimensions = Cubicle::MemberList.new
10
+ @measures = Cubicle::MemberList.new
11
+ @source_collection_name = @cubicle.target_collection_name
12
+ @where, @from_date, @to_date, @date_dimension, @time_period, @limit, @offset = nil
13
+ @all_dimensions, @all_measures = false, false
14
+ @transient = false
15
+ @by=[]
16
+ @order_by=[]
17
+ @from_date_filter = "$gte"
18
+ @to_date_filter = "$lte"
19
+ end
20
+
21
+ def clone
22
+ Marshal.load(Marshal.dump(self))
23
+ end
24
+
25
+ def select_all
26
+ select :all_dimensions, :all_measures
27
+ end
28
+
29
+ def selected?(member = nil)
30
+ return (@dimensions.length > 0 || @measures.length > 0) unless member
31
+ member_name = member.kind_of?(Cubicle::Member) ? member.name : member.to_s
32
+ return @dimensions[member_name] ||
33
+ @measures[member_name]
34
+ end
35
+
36
+ def transient?
37
+ @transient || @cubicle.transient?
38
+ end
39
+
40
+ def transient!
41
+ @transient = true
42
+ @source_collection_name = nil
43
+ end
44
+
45
+ def all_measures?
46
+ @all_measures
47
+ end
48
+
49
+ def all_dimensions?
50
+ @all_dimensions
51
+ end
52
+
53
+ def select(*args)
54
+ args = args[0] if args[0].is_a?(Array)
55
+
56
+ if (args.include?(:all))
57
+ select_all
58
+ return
59
+ end
60
+
61
+ if (args.include?(:all_measures))
62
+ @all_measures = true
63
+ @measures = Cubicle::MemberList.new
64
+ end
65
+ if (args.include?(:all_dimensions))
66
+ @all_dimensions = true
67
+ @dimensions = Cubicle::MemberList.new
68
+ end
69
+
70
+ return if args.length == 1 && selected?(args[0])
71
+
72
+ found=[:all_measures,:all_dimensions]
73
+
74
+ if args.length == 1 && !all_dimensions? && args[0].is_a?(Cubicle::Dimension)
75
+ @dimensions << convert_dimension(args.pop)
76
+ elsif args.length == 1 && !all_measures? && args[0].is_a?(Cubicle::Measure)
77
+ @measures << convert_measure(args.pop)
78
+ else
79
+ #remove from the list any dimensions or measures that are already
80
+ #selected. This allows select to be idempotent,
81
+ #which is useful for ensuring certain members are selected
82
+ #even though the user may already have selected them previously
83
+ args.each do |member_name|
84
+ if (member = @cubicle.dimensions[member_name])
85
+ @dimensions << convert_dimension(member)
86
+ elsif (member = @cubicle.measures[member_name])
87
+ @measures << convert_measure(member)
88
+ end
89
+ found << member_name if member || selected?(member_name)
90
+ end
91
+ end
92
+ args = args - found
93
+ raise "You selected one or more members that do not exist in the underlying data source:#{args.inspect}" unless args.blank?
94
+ self
95
+ end
96
+
97
+ def limit(in_limit = nil)
98
+ return @limit unless in_limit
99
+ @limit = in_limit
100
+ return self
101
+ end
102
+
103
+ def offset(in_offset = nil)
104
+ return @offset unless in_offset
105
+ @offset = in_offset
106
+ return self
107
+ end
108
+ alias skip offset
109
+
110
+ def by(*args)
111
+ return @by unless args.length > 0
112
+
113
+ #We'll need these in the result set
114
+ select *args
115
+
116
+ #replace any alias names with actual member names
117
+ @by = args.map{|member_name|@cubicle.find_member(member_name).name}
118
+ return if @time_dimension #If a time dimension has been explicitly specified, the following isn't helpful.
119
+
120
+ #Now let's see if we can find ourselves a time dimension
121
+ # if (@cubicle.time_dimension && time_dimension.included_in?(args))
122
+ # time_dimension(@cubicle.time_dimension)
123
+ # else
124
+ # args.each do |by_member|
125
+ # if (detected = detect_time_period by_member)
126
+ # time_dimension by_member
127
+ # @time_period = detected
128
+ # break
129
+ # end
130
+ # end
131
+ # end
132
+ end
133
+
134
+ def order_by(*args)
135
+ return @order_by unless args.length > 0
136
+ args.each do |order|
137
+ @order_by << (order.is_a?(Array) ? order : [order,:asc])
138
+ end
139
+ end
140
+
141
+ def time_range(date_range = nil)
142
+ return nil unless date_range || @from_date || @to_date
143
+ return ((@from_date || Time.now)..(@to_date || Time.now)) unless date_range
144
+ @to_date_filter = date_range.exclude_end? ? "$lt" : "$lte"
145
+ @from_date, @to_date = date_range.first, date_range.last if date_range
146
+ end
147
+
148
+ def time_dimension(dimension = nil)
149
+ return (@time_dimension ||= @cubicle.time_dimension) unless dimension
150
+ @time_dimension = dimension.is_a?(Cubicle::Dimension) ? dimension : @cubicle.dimensions[dimension]
151
+ raise "No dimension matching the name #{dimension} could be found in the underlying data source" unless @time_dimension
152
+ #select @time_dimension unless selected?(dimension)
153
+ end
154
+ alias date_dimension time_dimension
155
+
156
+ def last(duration,as_of = Time.now)
157
+ duration = 1.send(duration) if [:year,:month,:week,:day].include?(duration)
158
+ period = duration.parts[0][0]
159
+ @from_date = duration.ago(as_of).advance(period=>1)
160
+ @to_date = as_of
161
+ end
162
+ alias for_the_last last
163
+
164
+ def last_complete(duration,as_of = Time.now)
165
+ duration = 1.send(duration) if [:year,:month,:week,:day].include?(duration)
166
+ period = duration.parts[0][0]
167
+ @to_date = as_of.beginning_of(period)
168
+ @from_date = duration.ago(@to_date)
169
+ @to_date_filter = "$lt"
170
+ end
171
+ alias for_the_last_complete last_complete
172
+
173
+ def next(duration,as_of = Time.now)
174
+ duration = 1.send(duration) if [:year,:month,:week,:day].include?(duration)
175
+ period = duration.parts[0][0]
176
+ @to_date = duration.from_now(as_of).advance(period=>-1)
177
+ @from_date = as_of
178
+ end
179
+ alias for_the_next next
180
+
181
+ def this(period,as_of = Time.now)
182
+ @from_date = as_of.beginning_of(period)
183
+ @to_date = as_of
184
+ self
185
+ end
186
+
187
+ def from(time = nil)
188
+ return @from_date unless time
189
+ @from_date = if time.is_a?(Symbol)
190
+ Time.send(time) if Time.respond_to?(time)
191
+ Date.send(time).to_time if Date.respond_to?(time)
192
+ else
193
+ time.to_time
194
+ end
195
+ self
196
+ end
197
+
198
+ def until(time = nil)
199
+ return @to_date unless time
200
+ @to_date = if time.is_a?(Symbol)
201
+ Time.send(time) if Time.respond_to?(time)
202
+ Date.send(time).to_time if Date.respond_to?(time)
203
+ else
204
+ time.to_time
205
+ end
206
+ self
207
+ end
208
+
209
+ def ytd(as_of = Time.now)
210
+ this :year, as_of
211
+ end
212
+ alias year_to_date ytd
213
+
214
+ def mtd(as_of = Time.now)
215
+ this :month, as_of
216
+ end
217
+ alias month_to_date mtd
218
+
219
+ def where(filter = nil)
220
+ return prepare_filter unless filter
221
+ (@where ||= {}).merge!(filter)
222
+ self
223
+ end
224
+
225
+ def dimension_names
226
+ return dimensions.map{|dim|dim.name.to_s}
227
+ end
228
+
229
+ def member_names
230
+ return (dimensions + measures).map{|m|m.name.to_s}
231
+ end
232
+
233
+ def dimensions
234
+ return @dimensions unless all_dimensions?
235
+ @cubicle.dimensions.collect{|dim|convert_dimension(dim)}
236
+ end
237
+
238
+ def measures
239
+ return @measures unless all_measures?
240
+ @cubicle.measures.collect{|measure|convert_measure(measure)}
241
+ end
242
+
243
+ def execute(options={})
244
+ @cubicle.execute_query(self,options)
245
+ end
246
+
247
+ private
248
+ def prepare_filter
249
+ if @from_date || @to_date
250
+ unless time_dimension
251
+ raise "A date range was specified for this query (#{@from_date}->#{@to_date}) however, a time dimension wasn't detected. Please use the time_dimension directive to name a field in your source data that represents the date/time you want to use to filter your query"
252
+ end
253
+ @time_period ||= detect_time_period || :date
254
+
255
+ if transient? && time_dimension.expression_type != :field_name
256
+ raise "You are attempting to filter against the derived dimension (#{time_dimension.name}=#{time_dimension.expression}) in a transient query. This is not allowed in transient queries, which only allow filtering fields specified using :field_name"
257
+ end
258
+
259
+ time_filter = {}
260
+
261
+ dim_name = time_dimension.name
262
+ time_filter[@from_date_filter]=@from_date.utc.to_cubicle(@time_period) if @from_date
263
+ time_filter[@to_date_filter]=@to_date.utc.to_cubicle(@time_period) if @to_date
264
+ (@where ||= {})[dim_name] = time_filter
265
+ end
266
+ @where
267
+ end
268
+
269
+ def detect_time_period(dimension_name = (time_dimension ? time_dimension.name : nil))
270
+ return nil unless dimension_name
271
+ return case dimension_name.to_s.singularize
272
+ when /.*month$/ then :month
273
+ when /.*year$/ then :year
274
+ when /.*quarter$/ then :quarter
275
+ when /.*day$/ then :date
276
+ when /.*date$/ then :date
277
+ else nil
278
+ end
279
+ end
280
+
281
+ def convert_dimension(dimension)
282
+ return dimension if transient?
283
+ Cubicle::Dimension.new(dimension.name, :expression=>"this._id.#{dimension.name}")
284
+ end
285
+
286
+ def convert_measure(measure)
287
+
288
+ #If the measure is a ratio, we want to make
289
+ #sure each of the ratio components will be in the output
290
+ #Other than that, no change is required to the measure
291
+ #However, if all measures are included, this won't be necessary
292
+ #and would cancel out the implicit all_member inclusion. If this
293
+ #causes a bug down the line, we may need a specific :all_members
294
+ #flag rather than the implicit "no selections means all members"
295
+ #shortcut.
296
+ if (measure.is_a?(Cubicle::Ratio))
297
+ select measure.numerator, measure.denominator unless all_measures?
298
+ return measure
299
+ end
300
+
301
+ return measure if transient?
302
+
303
+ #when selecting from a cached map_reduce query, we no longer want to count rows, but aggregate
304
+ #the pre-calculated counts stored in the cached collection. Therefore, any :counts become :sum
305
+ aggregation = measure.aggregation_method == :count ? :sum : measure.aggregation_method
306
+ expression = "this.value.#{measure.name}"
307
+ if (aggregation == :average)
308
+ count_field = expression + "_count"
309
+ expression = "#{expression}*#{count_field}"
310
+ end
311
+ Cubicle::Measure.new(measure.name, :expression=>expression,:aggregation_method=>aggregation)
312
+ end
313
+
314
+ end
315
+ end