active_olap 0.0.2

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,230 @@
1
+ module ActiveOLAP
2
+
3
+ class Dimension
4
+
5
+ attr_reader :klass
6
+ attr_reader :categories
7
+ attr_reader :category_field
8
+ attr_reader :info
9
+
10
+ attr_reader :joins
11
+ attr_reader :conditions
12
+
13
+ # creates a new Dimension, given a definition.
14
+ # The definition can be:
15
+ # - A name (Symbol) of registered definition
16
+ # - A name (Symbol) of a field in the corresponding table
17
+ # - A hash, with at most one of the following keys set
18
+ # - :categories -> for custom categories
19
+ # - :trend -> for a trend dimension
20
+ # - :field -> for a table field dimension (similar to passing a Symbol)
21
+ def self.create(klass, definition = nil)
22
+ return klass if klass.kind_of? Dimension
23
+
24
+ case definition
25
+ when Dimension
26
+ return definition
27
+ when Symbol
28
+ if klass.active_olap_dimensions.has_key?(definition)
29
+ if klass.active_olap_dimensions[definition].respond_to?(:call)
30
+ return Dimension.new(klass, klass.active_olap_dimensions[definition].call)
31
+ else
32
+ return Dimension.new(klass, klass.active_olap_dimensions[definition])
33
+ end
34
+ else
35
+ return Dimension.new(klass, definition)
36
+ end
37
+ when Array
38
+ if klass.active_olap_dimensions.has_key?(definition.first)
39
+ return Dimension.new(klass, klass.active_olap_dimensions[definition.shift].call(*definition))
40
+ else
41
+ return Dimension.new(klass, definition)
42
+ end
43
+ else
44
+ return Dimension.new(klass, definition)
45
+ end
46
+ end
47
+
48
+ # Builds a SUM expression for this dimension.
49
+ def to_count_with_overlap_sql
50
+
51
+ count_value = @klass.connection.send(:quote_table_name, @klass.table_name) + '.' +
52
+ @klass.connection.send(:quote_column_name, :id) # TODO: other column than id
53
+
54
+ @categories.map { |category| category.to_count_sql(count_value) }.join(', ')
55
+ end
56
+
57
+ # Builds a CASE expression for this dimension.
58
+ def to_case_expression(variable_name)
59
+ if @category_field
60
+ "#{@klass.connection.send(:quote_column_name, @category_field)} AS #{@klass.connection.send(:quote_column_name, variable_name)}"
61
+ else
62
+ whens = @categories.map { |category| @klass.send(:sanitize_sql, ["WHEN (#{category.to_sanitized_sql}) THEN ?", category.label.to_s]) }
63
+ "CASE #{whens.join(' ')} ELSE NULL END AS #{@klass.connection.send(:quote_column_name, variable_name)}";
64
+ end
65
+ end
66
+
67
+ # Registers a category in this dimension.
68
+ # This function is called when a dimension is created and the categories are known or
69
+ # while the query result is being populated for dimensions with unknown categories.
70
+ def register_category(cat_label, definition = nil)
71
+ unless has_category?(cat_label)
72
+ definition = {:expression => { @category_field => cat_label }} if definition.nil? && @category_field
73
+ cat = Category.new(self, cat_label, definition)
74
+ @categories << cat
75
+ return (@categories.length - 1)
76
+ else
77
+ return category_index(cat_label)
78
+ end
79
+ end
80
+
81
+ # Returns a category, given a category label
82
+ def [](label)
83
+ category_by_label(label)
84
+ end
85
+
86
+ # Returns all the category labels
87
+ def category_labels
88
+ @categories.map(&:label)
89
+ end
90
+
91
+ # Checks whether this dimension has a category with the provided label
92
+ def has_category?(label)
93
+ @categories.any? { |cat| cat.label == label }
94
+ end
95
+
96
+ # Returns the index in this dimension of a category identified by its label
97
+ def category_index(label)
98
+ @categories.each_with_index { |cat, index| return index if cat.label == label }
99
+ return nil
100
+ end
101
+
102
+ # Returns a category, given its index
103
+ def category_by_index(index)
104
+ @categories[index]
105
+ end
106
+
107
+ # Returns a category, given a category label
108
+ def category_by_label(label)
109
+ @categories.detect { |cat| cat.label == label }
110
+ end
111
+
112
+ # checks whether thios is a table field dimension which categories are unknown beforehand
113
+ def is_field_dimension?
114
+ !@category_field.nil?
115
+ end
116
+
117
+ def is_time_dimension?
118
+ @info.has_key?(:trend) && @info[:trend] == true
119
+ end
120
+
121
+ def is_custom_dimension?
122
+ !is_field_dimension? && !is_time_dimension?
123
+ end
124
+
125
+ def has_overlap?
126
+ @info.has_key?(:overlap) && @info[:overlap] == true
127
+ end
128
+
129
+ # Returns a sanitized SQL expression for a given category
130
+ def sanitized_sql_for(cat)
131
+ cat_conditions = is_field_dimension? ? @klass.send(:sanitize_sql, { @category_field => cat }) : self[cat].to_sanitized_sql
132
+ @klass.send(:merge_conditions, cat_conditions, @conditions)
133
+ end
134
+
135
+ protected
136
+
137
+ # Generates an SQL expression for the :other-category
138
+ def generate_other_condition
139
+ all_categories = @categories.map { |category| "(#{category.to_sanitized_sql})" }.join(' OR ')
140
+ "((#{all_categories}) IS NULL OR NOT(#{all_categories}))"
141
+ end
142
+
143
+ # Initializes a new Dimension object. See Dimension#create
144
+ def initialize(klass, definition)
145
+ @klass = klass
146
+ @categories = []
147
+
148
+ @info = {}
149
+
150
+ @joins = []
151
+ @conditions = nil
152
+
153
+ case definition
154
+ when Hash
155
+ hash = definition.clone
156
+ @conditions = hash.delete(:conditions)
157
+ @joins += hash[:joins].kind_of?(Array) ? hash.delete(:joins) : [hash.delete(:joins)] if hash.has_key?(:joins)
158
+
159
+ if hash.has_key?(:categories)
160
+ generate_custom_categories(hash.delete(:categories))
161
+
162
+ elsif hash.has_key?(:trend)
163
+ generate_trend_categories(hash.delete(:trend))
164
+
165
+ elsif hash.has_key?(:field)
166
+ generate_field_dimension(hash.delete(:field))
167
+
168
+ else
169
+ raise "Invalid category definition! " + definition.inspect
170
+ end
171
+
172
+ # make the remaining fields available in the info object
173
+ @info.merge!(hash)
174
+
175
+ when Symbol
176
+ generate_field_dimension(definition)
177
+
178
+ else
179
+ raise "Invalid category definition! " + definition.inspect
180
+ end
181
+ end
182
+
183
+ def generate_field_dimension(field)
184
+ case field
185
+ when Hash
186
+ @category_field = field[:column].to_sym
187
+ else
188
+ @category_field = field.to_sym
189
+ end
190
+
191
+ unless @klass.column_names.include?(@category_field.to_s)
192
+ raise "Could not create dimension for unknown field #{@category_field}"
193
+ end
194
+ end
195
+
196
+ def generate_custom_categories(categories)
197
+ skip_other = false
198
+ categories.to_a.each do |category|
199
+ skip_other = true if category.first == :other
200
+ register_category(category.first, category.last) if category.last
201
+ end
202
+ register_category(:other, :expression => generate_other_condition) unless skip_other
203
+ end
204
+
205
+ def generate_trend_categories(trend_definition)
206
+ period_count = trend_definition.delete(:periods) || trend_definition.delete(:period_count) || 14
207
+ period_length = trend_definition.delete(:period_length) || 1.days
208
+ trend_end = trend_definition.delete(:end) || Time.now.utc.midnight + 1.day
209
+ trend_begin = trend_definition.delete(:begin)
210
+ timestamp_field = trend_definition.delete(:timestamp_field)
211
+
212
+ if !trend_end.nil? && trend_begin.nil?
213
+ trend_begin = trend_end - (period_count * period_length)
214
+ end
215
+
216
+
217
+ field = @klass.connection.send :quote_column_name, timestamp_field
218
+ period_begin = trend_begin
219
+ period_count.times do |i|
220
+ register_category("period_#{i}".to_sym, {:begin => period_begin, :end => period_begin + period_length,
221
+ :expression => ["#{field} >= ? AND #{field} < ?", period_begin, period_begin + period_length] })
222
+ period_begin += period_length
223
+ end
224
+
225
+ # update conditions by only querying records that fall in any periods.
226
+ @conditions = @klass.send(:merge_conditions, @conditions, ["#{field} >= ? AND #{field} < ?", trend_begin, period_begin])
227
+ @info[:trend] = true
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,99 @@
1
+ module ActiveOLAP::Helpers
2
+ module ChartHelper
3
+
4
+ def included(base)
5
+ require 'gchartrb'
6
+ base.send :include, ActiveOLAP::Helpers::DisplayHelper
7
+ end
8
+
9
+ def active_olap_pie_chart(cube, options = {}, html_options = {})
10
+ raise "Pie charts are only suitable charts for 1-dimensional cubes" if cube.depth > 1
11
+ raise "Pie charts are only supported with a single aggregate" if cube.aggregates.length > 1
12
+
13
+ active_olap_1d_pie_chart(cube, options, html_options)
14
+ end
15
+
16
+ def active_olap_line_chart(cube, options = {}, html_options = {})
17
+ raise "Line charts are only supported with a single aggregate" if cube.aggregates.length > 1
18
+
19
+ case cube.depth
20
+ when 1; active_olap_1d_line_chart(cube, options, html_options)
21
+ when 2; active_olap_2d_line_chart(cube, options, html_options)
22
+ else; raise "Multidimensional line charts are not yet supported"
23
+ end
24
+ end
25
+
26
+ def active_olap_1d_pie_chart(cube, options = {}, html_options = {})
27
+
28
+ # set some default options
29
+ options[:size] ||= '550x300'
30
+ options[:legend] = true unless options.has_key?(:legend)
31
+ html_options[:alt] ||= show_active_olap_cube(cube, :for => :pie_chart)
32
+ html_options[:size] ||= options[:size]
33
+
34
+ chart = GoogleChart::PieChart.new(options[:size])
35
+ chart.show_legend = options[:legend]
36
+ cube.each do |category, value|
37
+ if category.info[:color]
38
+ color = category.info[:color]
39
+ color = $1 if color =~ /^\#([A-f0-9]{6})$/
40
+ chart.data show_active_olap_category(category, :for => :pie_chart), value, color
41
+ else
42
+ chart.data show_active_olap_category(category, :for => :pie_chart), value
43
+ end
44
+ end
45
+ image_tag(chart.to_url, html_options)
46
+ end
47
+
48
+ def active_olap_1d_line_chart(cube, options = {}, html_options = {})
49
+
50
+ # set some default options
51
+ options[:size] ||= '550x300'
52
+ options[:legend] = false unless options.has_key?(:legend)
53
+ html_options[:alt] ||= show_active_olap_cube(cube, :for => :line_chart)
54
+ html_options[:size] ||= options[:size]
55
+
56
+ chart = GoogleChart::LineChart.new(options[:size])
57
+ labels = cube.categories.map { |cat| show_active_olap_period(cat, :for => :line_chart) }
58
+
59
+ color = options[:color] || '000000'
60
+ color = $1 if color =~ /^\#([A-f0-9]{6})$/
61
+ chart.data(show_active_olap_dimension(cube.dimension, :for => :line_chart), cube.to_a, color)
62
+ chart.show_legend = options[:legend]
63
+ chart.axis :x, :labels => labels
64
+ chart.axis :y, :range => [0, cube.raw_results.max]
65
+ image_tag(chart.to_url, html_options)
66
+ end
67
+
68
+ def active_olap_2d_line_chart(cube, options = {}, html_options = {})
69
+
70
+ # set some default options
71
+ options[:size] ||= '550x300'
72
+ options[:legend] = true unless options.has_key?(:legend) && options[:legend] == false
73
+ colors = options.has_key?(:colors) ? options[:colors].clone : ['aaaaaa', 'aa0000', '00aa00', '0000aa', '222222']
74
+ html_options[:alt] ||= show_active_olap_cube(cube, :for => :line_chart)
75
+ html_options[:size] ||= options[:size]
76
+
77
+ chart = GoogleChart::LineChart.new(options[:size])
78
+
79
+ cube.transpose.each do |cat, sub_cube|
80
+ color = cat.info[:color] || colors.shift || '666666'
81
+ color = $1 if color =~ /^\#([A-f0-9]{6})$/
82
+ chart.data show_active_olap_category(cat, :for => :line_chart), sub_cube.raw_results, color
83
+ end
84
+
85
+ if options[:totals]
86
+ totals_label = options[:totals_label] || "Total"
87
+ totals_color = options[:totals_color] || "000000"
88
+ totals_color = $1 if totals_color =~ /^\#([A-f0-9]{6})$/
89
+ chart.data totals_label, cube.map { |cat, result| result.raw_results.sum }, totals_color
90
+ end
91
+
92
+ chart.show_legend = options[:legend]
93
+ labels = cube.categories.map { |cat| show_active_olap_period(cat, :for => :line_chart) }
94
+ chart.axis :x, :labels => labels
95
+ chart.axis :y, :range => [0, cube.raw_results.flatten.max]
96
+ image_tag(chart.to_url, html_options)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,54 @@
1
+ module ActiveOLAP::Helpers
2
+ module DisplayHelper
3
+
4
+
5
+ def show_active_olap_category(category, options = {})
6
+ return category.info[:name] unless category.info[:name].blank?
7
+ return show_active_olap_period(category, options) if category.dimension.is_time_dimension?
8
+ return category.to_s.humanize if category.label.kind_of?(Symbol)
9
+ return category.to_s
10
+ end
11
+
12
+ def show_active_olap_period(category, options = {})
13
+
14
+ duration = category.info[:end] - category.info[:begin]
15
+ if duration < 1.hour
16
+ begin_time = category.info[:begin].strftime('%H:%M')
17
+ end_time = category.info[:end].strftime('%H:%M')
18
+ elsif duration < 1.day
19
+ begin_time = category.info[:begin].strftime('%H')
20
+ end_time = category.info[:end].strftime('%H')
21
+ elsif duration < 1.month
22
+ begin_time = category.info[:begin].strftime('%m/%d')
23
+ end_time = category.info[:end].strftime('%m/%d')
24
+ else
25
+ begin_time = category.info[:begin].strftime('\'%y/%m/%d')
26
+ end_time = category.info[:end].strftime('\'%y/%m/%d')
27
+ end
28
+
29
+ case options[:for]
30
+ when :line_chart; begin_time
31
+ else; "#{begin_time} - #{end_time}"
32
+ end
33
+ end
34
+
35
+
36
+ def show_active_olap_aggregate(aggregate, options = {})
37
+ aggregate.info[:name].blank? ? aggregate.label.to_s : aggregate.info[:name]
38
+ end
39
+
40
+ def show_active_olap_value(category, aggregate, value, options = {})
41
+ value.nil? ? '-' : value.to_s
42
+ end
43
+
44
+ def show_active_olap_dimension(dimension, options = {})
45
+ return dimension.info[:name] unless dimension.info[:name].blank?
46
+ "dimension"
47
+ end
48
+
49
+ def show_active_olap_cube(cube, options = {})
50
+ return cube.info[:name] unless cube.info[:name].blank?
51
+ "OLAP cube"
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,16 @@
1
+ module ActiveOLAP::Helpers
2
+ module FormHelper
3
+
4
+ def select_olap_dimension_tag(klass, dim_index = 1, options = {}, html_options = {})
5
+ dimensions = klass.active_olap_dimensions.map { |l, dim| [l, ActiveOLAP::Dimension.create(klass, l)] }
6
+ dimensions.delete_if { |(l, dim)| dim.is_time_dimension? }.map { |(l, dim)| [l, show_active_olap_dimension(dim)] }
7
+ select_tag "dimension[#{dim_index}][name]", options_for_select(dimensions, nil), html_options
8
+ end
9
+
10
+ def select_olap_time_dimension_tag(klass, dim_index = 1, options = {}, html_options = {})
11
+ dimensions = klass.active_olap_dimensions.map { |l, dim| [l, ActiveOLAP::Dimension.create(klass, l)] }
12
+ dimensions.delete_if { |(l, dim)| !dim.is_time_dimension? }.map { |(l, dim)| [l, show_active_olap_dimension(dim)] }
13
+ select_tag "dimension[#{dim_index}][name]", options_for_select(dimensions, nil), html_options
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,92 @@
1
+ module ActiveOLAP::Helpers
2
+ module TableHelper
3
+
4
+
5
+ def self.included(base)
6
+ base.send :include, ActiveOLAP::Helpers::DisplayHelper
7
+ end
8
+
9
+ def active_olap_matrix(cube, options = {}, html_options = {})
10
+
11
+ raise "Only suitable for 2D cubes" unless cube.depth == 2
12
+ raise "Only 1 aggregate supported" if cube.aggregates.length > 1
13
+ options[:strings] ||= {}
14
+
15
+ content_tag(:table, html_options.merge(:class => "active-olap table 2d")) do
16
+ content_tag(:thead) do
17
+ content_tag(:tr, :class => 'category') do
18
+ header_html = content_tag(:th, '&nbsp;') + "\n\t" +
19
+ cube.dimensions[1].categories.map do |category|
20
+ content_tag(:th, show_active_olap_category(category, :for => :matrix), :class => 'column', :id => "category-#{category.label}")
21
+ end.join
22
+
23
+ if options[:totals]
24
+ header_html << content_tag(:th, '&nbsp;', :class => 'column total', :id => "category-total")
25
+ end
26
+ header_html
27
+ end
28
+ end << "\n" <<
29
+ content_tag(:tbody) do
30
+ body_html = cube.map do |category, sub_cube|
31
+ content_tag(:tr, :class => 'row') do
32
+ row_html = "\t\n" + content_tag(:th, show_active_olap_category(category, :class => 'category row', :id => "category-#{category.label}", :for => :matrix)) +
33
+ sub_cube.map do |category, value|
34
+ content_tag(:td, show_active_olap_value(category, cube.aggregates.first, value, :for => :matrix), :class => 'value')
35
+ end.join
36
+ if options[:totals]
37
+ row_html << content_tag(:td, show_active_olap_value(category, sub_cube.aggregates.first, sub_cube.sum, :for => :matrix), :class => 'value')
38
+ end
39
+ row_html
40
+ end
41
+ end
42
+
43
+ if options[:totals]
44
+ body_html << content_tag(:tr, :class => 'totals') do
45
+ "\t\n" + content_tag(:th, '&nbsp;', :class => 'row total') +
46
+ cube.transpose.map do |category, sub_cube|
47
+ content_tag(:td, show_active_olap_value(category, sub_cube.aggregates.first, sub_cube.sum, :for => :matrix), :class => 'value')
48
+ end.join +
49
+ content_tag(:td, cube.sum, :class => 'value')
50
+ end
51
+ end
52
+ body_html
53
+ end
54
+ end
55
+ end
56
+
57
+ def active_olap_table(cube, options = {}, html_options = {})
58
+ content_tag(:table, :class => "active-olap table #{cube.depth}d" ) do
59
+ content_tag(:thead) do
60
+ content_tag(:tr) do
61
+ content_tag(:th, '&nbsp;', :class => 'categories', :colspan => cube.depth) <<
62
+ cube.aggregates.map { |agg| content_tag(:th, show_active_olap_aggregate(agg, :for => :table), :class => "aggregate #{agg.label}") }.join
63
+ end
64
+ end << "\n" <<
65
+ content_tag(:tbody) { active_olap_table_bodypart(cube, options) }
66
+ end
67
+ end
68
+
69
+ def active_olap_table_bodypart(cube, options = {}, intermediate = [], counts = [])
70
+ cube.map do |category, result|
71
+ if result.kind_of?(ActiveOLAP::Cube)
72
+ active_olap_table_bodypart(result, options, intermediate.push(category), counts.push(result.categories.length))
73
+ else
74
+ content_tag(:tr) do
75
+ cells = intermediate.map do |c|
76
+ cat_count = counts.shift
77
+ content_tag(:th, c.label.to_s, { :class => 'category', :rowspan => cat_count * counts.inject(1) { |i, count| i * count } } )
78
+ end.join
79
+ intermediate.clear
80
+
81
+ cells << content_tag(:th, show_active_olap_category(category, :for => :table), :class => "category") # TODO values
82
+ cells << if result.kind_of?(Hash)
83
+ cube.aggregates.map { |agg| content_tag(:td, show_active_olap_value(category, agg, result[agg.label], :for => :table), :class => "value #{agg.label}") }.join
84
+ else
85
+ content_tag(:td, show_active_olap_value(category, cube.aggregates[0], result, :for => :table), :class => 'value')
86
+ end
87
+ end
88
+ end
89
+ end.join
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,39 @@
1
+ module ActiveOLAP::Test
2
+ module Assertions
3
+
4
+ # tests whether Active OLAP is enabled for a given class, by checking
5
+ # whether the class responds to certain methods like olap_query
6
+ def assert_active_olap_enabled(klass, message = "Active OLAP is not enabled for this class!")
7
+ assert klass.respond_to?(:active_olap_dimensions), message
8
+ assert klass.respond_to?(:active_olap_aggregates), message
9
+ assert klass.respond_to?(:olap_query), message
10
+ assert klass.respond_to?(:olap_drilldown), message
11
+ end
12
+
13
+ # tests whether a given cube is a valid Active OLAP cube and has the expected dimensions
14
+ # you can specify the number of dimenions as an integer (say n), or as an array of n integers,
15
+ # in which each element is the number of categories that should be in that dimension. You can
16
+ # use :unknown if this is not known beforehand
17
+ #
18
+ # examples:
19
+ #
20
+ # assert_active_olap_cube cube, 2
21
+ # => checks for the existence of two dimensions
22
+ #
23
+ # assert_active_olap_cube cube, [3, :unknown]
24
+ # => Checks for the existence of two dimensions.
25
+ # - The first dimension should have 3 catgeories
26
+ # - The number of categories in the second dimension is unknown
27
+ def assert_active_olap_cube(cube, dimensions = nil)
28
+ assert_kind_of ActiveOLAP::Cube, cube
29
+ if dimensions.kind_of?(Array)
30
+ assert_equal dimensions.length, cube.depth
31
+ dimensions.each_with_index do |category_count, index|
32
+ assert_equal category_count, cube.dimensions[index].categories.length unless category_count == :unknown
33
+ end
34
+ elsif dimensions.kind_of?(Numeric)
35
+ assert_equal dimensions, cube.depth
36
+ end
37
+ end
38
+ end
39
+ end
@@ -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 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper.rb'
@@ -0,0 +1,14 @@
1
+ $:.reject! { |e| e.include? 'TextMate' }
2
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
3
+
4
+ # load RSpec libraries
5
+ require 'rubygems'
6
+ gem 'rspec', '>=1.1.11'
7
+ require 'test/unit'
8
+ require 'spec'
9
+
10
+
11
+ # Load Active OLAP files
12
+ require 'active_record'
13
+ require File.dirname(__FILE__) + '/../lib/active_record/olap'
14
+ require File.dirname(__FILE__) + '/../lib/active_record/olap/cube'
@@ -0,0 +1,11 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe ActiveOLAP::Cube do
4
+
5
+ before(:each) do
6
+
7
+ end
8
+
9
+ it "should blah"
10
+
11
+ end