active_olap 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/MIT-LICENSE +20 -0
- data/README.textile +59 -0
- data/Rakefile +5 -0
- data/active_olap.gemspec +15 -0
- data/init.rb +2 -0
- data/lib/active_olap/aggregate.rb +148 -0
- data/lib/active_olap/category.rb +46 -0
- data/lib/active_olap/configurator.rb +32 -0
- data/lib/active_olap/cube.rb +215 -0
- data/lib/active_olap/dimension.rb +230 -0
- data/lib/active_olap/helpers/chart_helper.rb +99 -0
- data/lib/active_olap/helpers/display_helper.rb +54 -0
- data/lib/active_olap/helpers/form_helper.rb +16 -0
- data/lib/active_olap/helpers/table_helper.rb +92 -0
- data/lib/active_olap/test/assertions.rb +39 -0
- data/lib/active_olap.rb +116 -0
- data/spec/integration/active_olap_spec.rb +1 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/unit/cube_spec.rb +11 -0
- data/tasks/github-gem.rake +312 -0
- data/test/active_olap_test.rb +335 -0
- data/test/helper.rb +115 -0
- data/test/helper_modules_test.rb +65 -0
- metadata +82 -0
data/.gitignore
ADDED
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
data/active_olap.gemspec
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'active_olap'
|
3
|
+
s.version = '0.0.2'
|
4
|
+
s.date = '2008-12-23'
|
5
|
+
|
6
|
+
s.summary = "Extend ActiveRecord with OLAP query functionality"
|
7
|
+
s.description = "Extends ActiveRecord with functionality to perform OLAP queries on your data. Includes helper method to ease displaying the results."
|
8
|
+
|
9
|
+
s.authors = ['Willem van Bergen']
|
10
|
+
s.email = ['willem@vanbergen.org']
|
11
|
+
s.homepage = 'http://github.com/wvanbergen/active_olap/wikis'
|
12
|
+
|
13
|
+
s.files = %w(test/helper_modules_test.rb spec/spec_helper.rb .gitignore lib/active_olap/helpers/table_helper.rb lib/active_olap/dimension.rb test/active_olap_test.rb lib/active_olap/helpers/display_helper.rb init.rb README.textile spec/integration/active_olap_spec.rb lib/active_olap/test/assertions.rb lib/active_olap/category.rb active_olap.gemspec Rakefile MIT-LICENSE tasks/github-gem.rake lib/active_olap.rb test/helper.rb lib/active_olap/helpers/form_helper.rb lib/active_olap/aggregate.rb spec/unit/cube_spec.rb lib/active_olap/helpers/chart_helper.rb lib/active_olap/cube.rb lib/active_olap/configurator.rb)
|
14
|
+
s.test_files = %w(test/helper_modules_test.rb test/active_olap_test.rb spec/integration/active_olap_spec.rb spec/unit/cube_spec.rb)
|
15
|
+
end
|
data/init.rb
ADDED
@@ -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
|