wvanbergen-active_olap 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Willem van Bergen
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.
data/README.textile ADDED
@@ -0,0 +1,59 @@
1
+ h1. Active OLAP
2
+
3
+ This Rails plugin makes it easy to add an OLAP interface to your application, which is
4
+ great for administration interfaces. Its main uses are collection information about the
5
+ usage of your application and detecting inconsistencies and problems in your data.
6
+
7
+ This plugin provides:
8
+ * The main functions for OLAP querying: olap_query and olap_drilldown. These functions
9
+ must be enabled for your model by calling enable_active_olap on your model class.
10
+ * Functions to easily define dimension, categories and aggregates to use in your
11
+ OLAP queries.
12
+
13
+ In the future, the following functionality is planned to be included:
14
+ * A helper module to generate tables and charts for the query results. The gchartrb gem
15
+ is needed for charts, as they are generated using the Google charts API.
16
+ * A controller that can be included in your Rails projects to get started quickly.
17
+
18
+ More information about the concepts and usage of this plugin, see the Active OLAP Wiki on
19
+ GitHub: http://github.com/wvanbergen/active_olap/wikis. I have blogged about this plugin
20
+ on the Floorplanner tech blog: http://techblog.floorplanner.com/tag/active_olap/. Finally,
21
+ if you want to get involved or tinker with the code, you can access the repository at
22
+ http://github.com/wvanbergen/active_olap/tree.
23
+
24
+
25
+ h2. Why use this plugin?
26
+
27
+ This plugin simply runs SQL queries using the find-method of ActiveRecord. You might be
28
+ wondering why you would need a plugin for that.
29
+
30
+ First of all, it makes your life as a developer easier:
31
+ * This plugin generates the nasty SQL expressions for you using standard compliant SQL,
32
+ handles issues with SQL NULL values and makes sure the results have a consistent format.
33
+ * You can define dimensions and aggregates that are "safe to use" or known to yield useful
34
+ results. Once dimensions and aggregates are defined, they can be combined at will safely
35
+ and without any coding effort, so it is suitable for management. :-)
36
+
37
+
38
+ h2. Requirements
39
+
40
+ This plugin is usable for any ActiveRecord-based model. Because named_scope is used for the
41
+ implementation, Rails 2.1 is required for it to work. It is tested to work with MySQL 5 and
42
+ SQLite 3 but should work with other databases as well, as it only generates standard
43
+ compliant SQL queries.
44
+
45
+ Warning: OLAP queries can be heavy on the database. They can impact the performance of your
46
+ application if you perform them on the same server or database. Setting good indices is
47
+ helpful, but it may be a good idea to use a copy of the production database on another
48
+ server for these heavy queries.
49
+
50
+ Another warning: while this plugin makes it easy to perform OLAP queries and play around
51
+ with it, interpreting the results is hard and mistakes are easily made. At least, make sure
52
+ to validate the results before they are used for decision making.
53
+
54
+
55
+ h2. About this plugin
56
+
57
+ The plugin is written by Willem van Bergen for Floorplanner.com. It is MIT-licensed (see
58
+ MIT-LICENSE). If you have any questions or want to help out with the development of this plugin,
59
+ please contact me on willem AT vanbergen DOT org.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ Dir[File.dirname(__FILE__) + "/tasks/*.rake"].each { |file| load(file) }
2
+
3
+ desc 'Default: run unit tests.'
4
+ task :default => :test
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/lib')
2
+ require 'active_olap'
@@ -0,0 +1,116 @@
1
+ module ActiveOLAP
2
+
3
+ def enable_active_olap(config = nil, &block)
4
+
5
+ self.send(:extend, ClassMethods)
6
+ self.named_scope :olap_drilldown, lambda { |hash| self.olap_drilldown_finder_options(hash) }
7
+
8
+ self.cattr_accessor :active_olap_dimensions, :active_olap_aggregates
9
+ self.active_olap_dimensions = {}
10
+ self.active_olap_aggregates = {}
11
+
12
+ if config.nil? && block_given?
13
+ conf = Configurator.new(self)
14
+ yield(conf)
15
+ end
16
+
17
+ end
18
+
19
+
20
+ module ClassMethods
21
+
22
+ # Performs an OLAP query that counts how many records do occur in given categories.
23
+ # It can be used for multiple dimensions
24
+ # It expects a list of category definitions
25
+ def olap_query(*args)
26
+
27
+ # set aggregates apart if they are given
28
+ aggregates_given = (args.last.kind_of?(Hash) && args.last.has_key?(:aggregate)) ? args.pop[:aggregate] : nil
29
+
30
+ # parse the dimensions
31
+ raise "You have to provide at least one dimension for an OLAP query" if args.length == 0
32
+ dimensions = args.collect { |d| Dimension.create(self, d) }
33
+
34
+ raise "Overlapping categories only supported in the last dimension" if dimensions[0..-2].any? { |d| d.has_overlap? }
35
+ raise "Only counting is supported with overlapping categories" if dimensions.last.has_overlap? && aggregates_given
36
+
37
+ if !aggregates_given
38
+ if dimensions.last.has_overlap?
39
+ aggregates = []
40
+ else
41
+ aggregates = [Aggregate.create(self, :the_olap_count_field, :count_distinct)]
42
+ end
43
+ else
44
+ aggregates = Aggregate.all_from_olap_query_call(self, aggregates_given)
45
+ end
46
+
47
+ conditions = self.send(:merge_conditions, *dimensions.map(&:conditions))
48
+ joins = (dimensions.map(&:joins) + aggregates.map(&:joins)).flatten.uniq
49
+ joins_clause = joins.empty? ? nil : self.send(:merge_joins, *joins)
50
+
51
+ selects = aggregates.map { |agg| agg.to_sanitized_sql }
52
+ groups = []
53
+
54
+ if aggregates.length > 0
55
+ dimensions_to_group = dimensions.clone
56
+ else
57
+ selects << dimensions.last.to_count_with_overlap_sql
58
+ dimensions_to_group = dimensions[0, dimensions.length - 1]
59
+ end
60
+
61
+ dimensions_to_group.each_with_index do |d, index|
62
+ var_name = "dimension_#{index}"
63
+ groups << self.connection.quote_column_name(var_name)
64
+ selects << d.to_case_expression(var_name)
65
+ end
66
+
67
+ group_clause = groups.length > 0 ? groups.join(', ') : nil
68
+ # TODO: having
69
+
70
+ olap_temporarily_set_join_type if joins_clause
71
+
72
+ query_result = self.scoped(:conditions => conditions).find(:all, :select => selects.join(', '),
73
+ :joins => joins_clause, :group => group_clause, :order => group_clause)
74
+
75
+ olap_temporarily_reset_join_type if joins_clause
76
+
77
+ return Cube.new(self, dimensions, aggregates, query_result)
78
+ end
79
+ end
80
+
81
+ protected
82
+
83
+ def olap_drilldown_finder_options(options)
84
+ raise "You have to provide at least one dimension for an OLAP query" if options.length == 0
85
+
86
+ # returns an options hash to create a scope (the named_scope :olap_drilldown)
87
+ conditions = options.map { |dim, cat| Dimension.create(self, dim).sanitized_sql_for(cat) }
88
+ { :select => connection.quote_table_name(table_name) + '.*', :conditions => self.send(:merge_conditions, *conditions) }
89
+ end
90
+
91
+ # temporarily use LEFT JOINs for specified :joins
92
+ def olap_temporarily_set_join_type
93
+ ActiveRecord::Associations::ClassMethods::InnerJoinDependency::InnerJoinAssociation.send(:define_method, :join_type) { "LEFT OUTER JOIN" }
94
+ end
95
+
96
+ # reset to INNER JOINs after query has finished
97
+ def olap_temporarily_reset_join_type
98
+ ActiveRecord::Associations::ClassMethods::InnerJoinDependency::InnerJoinAssociation.send(:define_method, :join_type) { "INNER JOIN" }
99
+ end
100
+
101
+ end
102
+
103
+
104
+ require 'active_olap/dimension'
105
+ require 'active_olap/category'
106
+ require 'active_olap/aggregate'
107
+ require 'active_olap/cube'
108
+ require 'active_olap/configurator'
109
+
110
+ require 'active_olap/helpers/display_helper'
111
+ require 'active_olap/helpers/table_helper'
112
+ require 'active_olap/helpers/chart_helper'
113
+ require 'active_olap/helpers/form_helper'
114
+
115
+ # inlcude the AcvtiveOLAP module in ActiveRecord::Base
116
+ ActiveRecord::Base.send(:extend, ActiveOLAP)
@@ -0,0 +1,148 @@
1
+ module ActiveOLAP
2
+
3
+ class Aggregate
4
+
5
+ attr_reader :klass
6
+ attr_reader :label
7
+
8
+ attr_reader :function
9
+ attr_reader :distinct
10
+ attr_reader :expression
11
+
12
+ attr_reader :joins
13
+ attr_reader :info
14
+
15
+ def self.all_from_olap_query_call(klass, aggregates_given)
16
+ aggregates_given = [aggregates_given] unless aggregates_given.kind_of?(Array)
17
+
18
+ return aggregates_given.map do |aggregate_definition|
19
+ if aggregate_definition.kind_of?(Symbol) && klass.active_olap_aggregates.has_key?(aggregate_definition)
20
+ Aggregate.from_configuration(klass, aggregate_definition)
21
+ else
22
+ Aggregate.create(klass, aggregate_definition.to_sym, aggregate_definition)
23
+ end
24
+ end
25
+ end
26
+
27
+ def initialize(klass, label, function, expression = nil, distinct = false)
28
+ @klass = klass
29
+ @label = label
30
+ @function = function
31
+ @expression = expression
32
+ @distinct = distinct
33
+ @joins = []
34
+ @info = {}
35
+ end
36
+
37
+
38
+ def self.create(klass, label, definition)
39
+ case definition
40
+ when Symbol
41
+ return from_symbol(klass, label, definition)
42
+ when String
43
+ return from_string(klass, label, definition)
44
+ when Hash
45
+ return from_hash(klass, label, definition)
46
+ else
47
+ raise "Invalid aggregate definition: #{definition.inspect}"
48
+ end
49
+ end
50
+
51
+ def self.from_configuration(klass, aggregate_name, label = nil)
52
+ label = aggregate_name.to_sym if label.nil?
53
+ if klass.active_olap_aggregates[aggregate_name].respond_to?(:call)
54
+ return Aggregate.create(klass, label, klass.active_olap_aggregates[aggregate_name].call)
55
+ else
56
+ return Aggregate.create(klass, label, klass.active_olap_aggregates[aggregate_name])
57
+ end
58
+ end
59
+
60
+ def self.from_hash(klass, label, hash)
61
+ hash = hash.clone
62
+ agg = Aggregate.create(klass, label, hash.delete(:expression))
63
+ agg.joins.concat(hash[:joins].kind_of?(Array) ? hash.delete(:joins) : [hash.delete(:joins)]) if hash.has_key?(:joins)
64
+ hash.each { |key, val| agg.info[key] = val }
65
+ return agg
66
+ end
67
+
68
+ def self.from_string(klass, label, sql_expression)
69
+ if sql_expression =~ /^(\w+)\((.+)\)$/
70
+ return Aggregate.new(klass, label, $1.downcase.to_sym, $2, false)
71
+ else
72
+ raise "Invalid aggregate SQL expression: " + sql_expression
73
+ end
74
+ end
75
+
76
+ def self.from_symbol(klass, label, aggregate_name)
77
+
78
+ case aggregate_name
79
+ when :count_all
80
+ return Aggregate.new(klass, label, :count, '*', false) # with table name?
81
+ when :count_distinct_all
82
+ return Aggregate.new(klass, label, :count, '*', true) # with table name?
83
+ when :count
84
+ return Aggregate.new(klass, label, :count, :id, false)
85
+ when :count_distinct
86
+ return Aggregate.new(klass, label, :count, :id, true)
87
+
88
+ else
89
+ parts = aggregate_name.to_s.split('_')
90
+ raise "Invalid aggregate name: #{symbol.inspect}" unless parts.length > 1
91
+
92
+ distinct = false
93
+ if parts[1] == 'distinct'
94
+ parts.delete_at(1)
95
+ distinct = true
96
+ end
97
+
98
+ raise "Invalid aggregate name: #{symbol.inspect}" unless parts.length >= 2
99
+ #TODO: check field name and function name?
100
+ return Aggregate.new(klass, label, parts[0].to_sym, parts[1..-1].join('_').to_sym, distinct)
101
+ end
102
+ end
103
+
104
+ def to_sanitized_sql
105
+ sql = @function.to_s.upcase! + '('
106
+ sql << 'DISTINCT ' if @distinct
107
+ sql << (@expression.kind_of?(Symbol) ? "#{quote_table}.#{quote_column(@expression)}" : @expression.to_s)
108
+ sql << ") AS #{quote_column(@label)}"
109
+ end
110
+
111
+ def is_count_with_overlap?
112
+ @function == :count_with_overlap
113
+ end
114
+
115
+ def cast_value(source)
116
+ return nil if source.nil?
117
+ (@function == :count) ? source.to_i : source.to_f # TODO: better?
118
+ end
119
+
120
+ def default_value
121
+ (@function == :count) ? 0 : nil # TODO: better?
122
+ end
123
+
124
+ def self.values(aggregates, source)
125
+ result = HashWithIndifferentAccess.new
126
+ aggregates.each { |agg| result[agg.label] = agg.cast_value(source[agg.label.to_s]) }
127
+ return (aggregates.length == 1) ? result[aggregates.first.label] : result
128
+ end
129
+
130
+ def self.default_values(aggregates)
131
+ return 0 if aggregates.empty? # count with overlap
132
+ result = HashWithIndifferentAccess.new
133
+ aggregates.each { |agg| result[agg.label] = agg.default_value }
134
+ return (aggregates.length == 1) ? result[aggregates.first.label] : result
135
+ end
136
+
137
+ protected
138
+
139
+ def quote_column(column)
140
+ @klass.connection.send(:quote_column_name, column.to_s)
141
+ end
142
+
143
+ def quote_table
144
+ @klass.connection.send(:quote_table_name, @klass.table_name)
145
+ end
146
+ end
147
+
148
+ end
@@ -0,0 +1,46 @@
1
+ module ActiveOLAP
2
+
3
+ class Category
4
+
5
+ attr_reader :dimension, :label, :conditions, :info
6
+
7
+ # initializes a category, given the dimension it belongs to, a label,
8
+ # and a definition. The definition should be a hash with at least the
9
+ # key expression set to a usable ActiveRecord#find conditions
10
+ def initialize(dimension, label, definition)
11
+ @dimension = dimension
12
+ @label = label
13
+ @info = {}
14
+
15
+ if definition.kind_of?(Hash) && definition.has_key?(:expression)
16
+ @conditions = definition[:expression]
17
+ @info = definition.reject { |k,v| k == :expression }
18
+ else
19
+ @conditions = definition
20
+ end
21
+ end
22
+
23
+ # Returns the index of this category in the corresponding dimension
24
+ def index
25
+ @dimension.category_index(@label)
26
+ end
27
+
28
+ # Returns a santized SQL expression for this category
29
+ def to_sanitized_sql
30
+ @dimension.klass.send(:sanitize_sql, @conditions)
31
+ end
32
+
33
+ def to_count_sql(count_what)
34
+ "COUNT(DISTINCT CASE WHEN (#{to_sanitized_sql}) THEN #{count_what} ELSE NULL END)
35
+ AS #{@dimension.klass.connection.send(:quote_column_name, label.to_s)}"
36
+ end
37
+
38
+ # Returns the label of this category as a string
39
+ def to_s
40
+ return "nil" if label.nil?
41
+ label.to_s
42
+ end
43
+
44
+ end
45
+
46
+ end
@@ -0,0 +1,32 @@
1
+ module ActiveOLAP
2
+
3
+ class Configurator
4
+
5
+ # initializes a OLAP::Configurator object, which is used in the block
6
+ # passed to the call enable_active_olap. It can be used to register
7
+ # dimensions and classes
8
+ def initialize(klass)
9
+ @klass = klass
10
+ end
11
+
12
+ # registers a dimension for the class it belongs to
13
+ def dimension(name, definition = nil)
14
+ definition = name.to_sym if definition.nil?
15
+ @klass.active_olap_dimensions[name] = definition
16
+ end
17
+
18
+ def time_dimension(name, field, defaults = {})
19
+ @klass.active_olap_dimensions[name] = Proc.new do |*options|
20
+ options = options.empty? ? {} : options.first
21
+ { :trend => defaults.merge(options).merge(:timestamp_field => field) }
22
+ end
23
+ end
24
+
25
+ # registers an aggregate for the class it belongs to
26
+ def aggregate(name, definition = nil, options = {})
27
+ definition = name if definition.nil?
28
+ agg_definition = options.merge(:expression => definition)
29
+ @klass.active_olap_aggregates[name] = agg_definition
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,215 @@
1
+ module ActiveOLAP
2
+
3
+ class Cube
4
+
5
+ attr_accessor :info
6
+ attr_accessor :klass
7
+ attr_accessor :dimensions
8
+ attr_accessor :aggregates
9
+
10
+ # Initializes a new OLAP cube.
11
+ def initialize(klass, dimensions, aggregates, query_result = nil)
12
+ @klass = klass
13
+ @dimensions = dimensions
14
+ @aggregates = aggregates
15
+ @info = {}
16
+
17
+ # populates the cube with the query rsult if it is provided.
18
+ unless query_result.nil?
19
+ @result = []
20
+ populate_result_with(query_result)
21
+ traverse_result_for_nils(@result)
22
+ end
23
+ end
24
+
25
+ # Sums up all the values in this cube
26
+ def sum(agg = nil)
27
+ raise "Please provide the aggregate you want to sum." if self.aggregates.length > 1 && agg.nil?
28
+ total_sum = 0
29
+ self.each do |cat, value|
30
+ total_sum += (value.kind_of?(Cube) ? value.sum : (agg.nil? ? value : value[agg]))
31
+ end
32
+ return total_sum
33
+ end
34
+
35
+ # Returns the total number of cells in this cube. Note that this does not take aggregates into
36
+ # account, so the result should me multiplied by the number of aggregates if you want to know
37
+ # the total number of (numeric) values.
38
+ def cell_count
39
+ dimensions.inject(1) { |intermediate, dimension| intermediate * dimension.categories.length }
40
+ end
41
+
42
+ # Returns a reference to the internal array that holds ther raw results of this cube.
43
+ # Altering this array will alter the internals of the cube-object, so make sure you know what
44
+ # you are doing. Use to_a if you want to obtain a copy of the internal array
45
+ def raw_results
46
+ @result
47
+ end
48
+
49
+ # Returns a clone of the internal array that holds ther raw results of this cube.
50
+ def to_a
51
+ @result.clone
52
+ end
53
+
54
+ # Returns the array of categories of the current (= first) dimension
55
+ def categories
56
+ @dimensions.first.categories
57
+ end
58
+
59
+ # Returns the current (first) dimension
60
+ def dimension
61
+ @dimensions.first
62
+ end
63
+
64
+ # Returns the number of dimensions in this cube
65
+ def depth
66
+ @dimensions.length
67
+ end
68
+
69
+ # Returns the number of categories in the current (= first) dimension
70
+ def breadth
71
+ @result.length
72
+ end
73
+
74
+ # Switches the dimensions of a two-dimensional cube
75
+ def transpose
76
+ raise "Can only transpose 2-dimensial results" unless depth == 2
77
+ result_object = Cube.new(@klass, [@dimensions.last, @dimensions.first], @aggregates)
78
+ result_object.result = @result.transpose
79
+ return result_object
80
+ end
81
+
82
+ def reorder_dimensions(*order)
83
+ # IMPLEMENT ME
84
+ end
85
+
86
+ def only_aggregate(aggregate_label)
87
+ # IMPLEMENT ME
88
+ end
89
+
90
+ def only_dimension(dimension_index)
91
+ # IMPLEMENT ME
92
+ end
93
+
94
+ def except_dimension(dimension_index)
95
+ # IMPLEMENT ME
96
+ end
97
+
98
+ # Returns a part of the cube or a single cell
99
+ # If the number of arguments matches the number of dimensions, a single cell is returned;
100
+ # If the number of arguments is less that the number of dimensons, this function will return
101
+ # a cube with (dimensions.length - args.length) dimensions.
102
+ def [](*args)
103
+ result = @result.clone
104
+ args.each_with_index do |cat_label, index|
105
+ cat_index = @dimensions[index].category_index(cat_label)
106
+ return nil if cat_index.nil?
107
+ result = result[cat_index]
108
+ end
109
+
110
+ if result.kind_of?(Array)
111
+ # build a new query_result object if not enoug dimensions were provided
112
+ result_object = Cube.new(@klass, @dimensions[args.length...@dimensions.length], @aggregates)
113
+ result_object.result = result
114
+ return result_object
115
+ else
116
+ return result
117
+ end
118
+ end
119
+
120
+ # Iterates over all the categories of the current dimension and their corresponding values.
121
+ # The provided block should take two arguments: the first one will be the Category and
122
+ # the second one will be the sub-cube or cell belonging to that category
123
+ def each(&block)
124
+ categories.each { |cat| yield(cat, self[cat.label]) }
125
+ end
126
+
127
+ # Maps all the categoies of the current dimension and their corresponding values. See each.
128
+ def map(&block)
129
+ result = []
130
+ categories.each { |cat| result << yield(cat, self[cat.label]) }
131
+ return result
132
+ end
133
+
134
+ protected
135
+
136
+ # Set the result by hand. For internal use
137
+ def result=(array)
138
+ @result = array
139
+ end
140
+
141
+ # Walks over all the rows of the resultset to build the cube.
142
+ def populate_result_with(query_result)
143
+
144
+ query_result.each do |row|
145
+
146
+ result = @result
147
+ values = row.attributes_before_type_cast
148
+ discard_data = false
149
+
150
+ (@dimensions.length - 1).times do |dim_index|
151
+
152
+ category_name = values.delete("dimension_#{dim_index}")
153
+ if @dimensions[dim_index].is_field_dimension?
154
+ # this field contains the value of the category_field, which should be used as category
155
+ # this might be the first time this category is seen, so register it in the dimension
156
+ category_index = @dimensions[dim_index].register_category(category_name)
157
+
158
+ elsif category_name.nil?
159
+ # this is a record for rows that did not fall in any of the categories of a dimension
160
+ # therefore, this data can be discarded. This should not happen if an "other"-field is present!
161
+ discard_data = true
162
+ break
163
+
164
+ else
165
+ # get the index of the category, which should exist
166
+ category_index = @dimensions[dim_index].category_index(category_name.to_sym)
167
+ end
168
+
169
+ # switch the result to the next dimension
170
+ result[category_index] = [] if result[category_index].nil? # add a new dimension if needed
171
+ result = result[category_index] # set the result to the next dimension for the next iteration
172
+ end
173
+
174
+ unless discard_data
175
+ dim = @dimensions.last # only the last dimension is remaining
176
+ if dim.is_field_dimension?
177
+ # the last dimension is a field category.
178
+ # every category is represented as a single row, with only one count per row
179
+ dimension_field_value = values["dimension_#{@dimensions.length - 1}"]
180
+ result[dim.register_category(dimension_field_value)] = Aggregate.values(@aggregates, values)
181
+
182
+ elsif aggregates.length == 0
183
+ # the last dimension is a category with possible overlap, using SUMs.
184
+ # every category will have its number on this row
185
+ result = [] if result.nil?
186
+ values.each { |key, value| result[dim.category_index(key.to_sym)] = value.to_i }
187
+
188
+ else
189
+ # the last category is a normal category
190
+ dimension_field_value = values["dimension_#{@dimensions.length - 1}"]
191
+ result[dim.category_index(dimension_field_value.to_sym)] = Aggregate.values(@aggregates, values) unless dimension_field_value.nil?
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ # Makes sure all the values are set in the resulting array
198
+ def traverse_result_for_nils(result, depth = 0)
199
+ dim = @dimensions[depth]
200
+ if dim == @dimensions.last
201
+ # set all categories to 0 if no value is set
202
+ dim.categories.length.times do |i|
203
+ result[i] = Aggregate.default_values(@aggregates) if result[i].nil?
204
+ end
205
+ else
206
+ # if no value set, create an empty array and iterate to the next dimension
207
+ # so all values will be set to 0
208
+ dim.categories.length.times do |i|
209
+ result[i] = [] if result[i].nil?
210
+ traverse_result_for_nils(result[i], depth + 1)
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end