active_olap 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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