activewarehouse 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +62 -17
- data/Rakefile +17 -0
- data/generators/bridge/USAGE +1 -0
- data/generators/bridge/bridge_generator.rb +46 -0
- data/generators/bridge/templates/fixture.yml +5 -0
- data/generators/bridge/templates/migration.rb +20 -0
- data/generators/bridge/templates/model.rb +3 -0
- data/generators/dimension/templates/unit_test.rb +0 -2
- data/generators/dimension_view/USAGE +1 -0
- data/generators/dimension_view/dimension_view_generator.rb +62 -0
- data/generators/dimension_view/templates/migration.rb +11 -0
- data/generators/dimension_view/templates/model.rb +3 -0
- data/generators/dimension_view/templates/unit_test.rb +10 -0
- data/init.rb +1 -0
- data/lib/active_warehouse.rb +24 -9
- data/lib/active_warehouse/{model/aggregate.rb → aggregate.rb} +29 -13
- data/lib/active_warehouse/builder/date_dimension_builder.rb +21 -6
- data/lib/active_warehouse/builder/random_data_builder.rb +204 -3
- data/lib/active_warehouse/compat/compat.rb +49 -0
- data/lib/active_warehouse/{model/cube.rb → cube.rb} +47 -17
- data/lib/active_warehouse/dimension.rb +296 -0
- data/lib/active_warehouse/dimension/bridge.rb +15 -0
- data/lib/active_warehouse/dimension/dimension_view.rb +11 -0
- data/lib/active_warehouse/dimension/hierarchical_dimension.rb +60 -0
- data/lib/active_warehouse/dimension/slowly_changing_dimension.rb +137 -0
- data/lib/active_warehouse/{model/fact.rb → fact.rb} +45 -10
- data/lib/active_warehouse/migrations.rb +1 -2
- data/lib/active_warehouse/report.rb +3 -0
- data/lib/active_warehouse/{model/report → report}/abstract_report.rb +0 -0
- data/lib/active_warehouse/{model/report → report}/chart_report.rb +0 -0
- data/lib/active_warehouse/{model/report → report}/table_report.rb +0 -0
- data/lib/active_warehouse/version.rb +2 -2
- data/lib/active_warehouse/view/report_helper.rb +2 -1
- data/tasks/active_warehouse_tasks.rake +54 -0
- metadata +43 -21
- data/doc/agg_queries.txt +0 -26
- data/doc/agg_queries_results.txt +0 -150
- data/doc/queries.txt +0 -35
- data/lib/active_warehouse/model.rb +0 -5
- data/lib/active_warehouse/model/dimension.rb +0 -3
- data/lib/active_warehouse/model/dimension/bridge.rb +0 -32
- data/lib/active_warehouse/model/dimension/dimension.rb +0 -152
- data/lib/active_warehouse/model/dimension/hierarchical_dimension.rb +0 -35
- data/lib/active_warehouse/model/report.rb +0 -3
@@ -1,18 +1,33 @@
|
|
1
|
-
module ActiveWarehouse
|
2
|
-
module Builder
|
1
|
+
module ActiveWarehouse #:nodoc:
|
2
|
+
module Builder #:nodoc:
|
3
|
+
# A builder which will build a data structure which can be used to populate a date dimension using
|
4
|
+
# commonly used date dimension columns.
|
3
5
|
class DateDimensionBuilder
|
4
|
-
|
6
|
+
# Specify the start date for the first record
|
7
|
+
attr_accessor :start_date
|
8
|
+
|
9
|
+
# Specify the end date for the last record
|
10
|
+
attr_accessor :end_date
|
11
|
+
|
12
|
+
# Define any holiday indicators
|
13
|
+
attr_accessor :holiday_indicators
|
14
|
+
|
15
|
+
# Define the weekday indicators. The default array begins on Sunday and goes to Saturday.
|
5
16
|
cattr_accessor :weekday_indicators
|
6
17
|
@@weekday_indicators = ['Weekend','Weekday','Weekday','Weekday','Weekday','Weekday','Weekend']
|
7
|
-
|
18
|
+
|
19
|
+
# Initialize the builder.
|
20
|
+
#
|
21
|
+
# * <tt>start_date</tt>: The start date. Defaults to 5 years ago from today.
|
22
|
+
# * <tt>end_date</tt>: The end date. Defaults to now.
|
8
23
|
def initialize(start_date=Time.now.years_ago(5), end_date=Time.now)
|
9
24
|
@start_date = start_date
|
10
25
|
@end_date = end_date
|
11
26
|
@holiday_indicators = []
|
12
27
|
end
|
13
28
|
|
14
|
-
# Returns an array of hashes representing records in the dimension
|
15
|
-
#
|
29
|
+
# Returns an array of hashes representing records in the dimension. The values for each record are
|
30
|
+
# accessed by name.
|
16
31
|
def build(options={})
|
17
32
|
records = []
|
18
33
|
date = start_date
|
@@ -1,12 +1,213 @@
|
|
1
|
-
module ActiveWarehouse
|
2
|
-
module Builder
|
1
|
+
module ActiveWarehouse #:nodoc:
|
2
|
+
module Builder #:nodoc:
|
3
|
+
# Build random data usable for testing.
|
3
4
|
class RandomDataBuilder
|
5
|
+
# Hash of generators where the key is the class and the value is an implementation of AbstractGenerator
|
6
|
+
attr_reader :generators
|
7
|
+
|
8
|
+
# Hash of names mapped to generators where the name is the column name
|
9
|
+
attr_reader :column_generators
|
10
|
+
|
4
11
|
def initialize
|
12
|
+
@generators = {
|
13
|
+
Fixnum => FixnumGenerator.new,
|
14
|
+
Float => FloatGenerator.new,
|
15
|
+
Date => DateGenerator.new,
|
16
|
+
Time => TimeGenerator.new,
|
17
|
+
String => StringGenerator.new,
|
18
|
+
Object => BooleanGenerator.new,
|
19
|
+
}
|
20
|
+
@column_generators = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def build(name, options={})
|
24
|
+
case name
|
25
|
+
when Class
|
26
|
+
if name.respond_to?(:base_class)
|
27
|
+
return build_dimension(name, options) if name.base_class == ActiveWarehouse::Dimension
|
28
|
+
return build_fact(name, options) if name.base_class == ActiveWarehouse::Fact
|
29
|
+
end
|
30
|
+
raise "#{name} is a class but does not appear to descend from Fact or Dimension"
|
31
|
+
when String
|
32
|
+
begin
|
33
|
+
build(name.classify.constantize, options)
|
34
|
+
rescue NameError
|
35
|
+
raise "Cannot find a class named #{name.classify}"
|
36
|
+
end
|
37
|
+
when Symbol
|
38
|
+
build(name.to_s, options)
|
39
|
+
else
|
40
|
+
raise "Unable to determine what to build"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Build test dimension data for the specified dimension name.
|
45
|
+
#
|
46
|
+
# Options:
|
47
|
+
# * <tt>:rows</tt>: The number of rows to create (defaults to 100)
|
48
|
+
# * <tt>:generators</tt>: A map of generators where each key is Fixnum, Float, Date, Time, String, or Object and the
|
49
|
+
# value is extends from AbstractGenerator.
|
50
|
+
def build_dimension(name, options={})
|
51
|
+
options[:rows] ||= 100
|
52
|
+
options[:generators] ||= {}
|
53
|
+
rows = []
|
54
|
+
|
55
|
+
dimension_class = Dimension.to_dimension(name)
|
56
|
+
options[:rows].times do
|
57
|
+
row = {}
|
58
|
+
dimension_class.content_columns.each do |column|
|
59
|
+
generator = (options[:generators][column.klass] || @column_generators[column.name] || @generators[column.klass])
|
60
|
+
row[column.name] = generator.generate(column, options)
|
61
|
+
end
|
62
|
+
rows << row
|
63
|
+
end
|
5
64
|
|
65
|
+
rows
|
6
66
|
end
|
7
67
|
|
8
|
-
|
68
|
+
# Build test fact data for the specified fact name
|
69
|
+
#
|
70
|
+
# Options:
|
71
|
+
# * <tt>:rows</tt>: The number of rows to create (defaults to 100)
|
72
|
+
# * <tt>:generators</tt>: A Hash of generators where each key is Fixnum, Float, Date, Time, String, or Object and the
|
73
|
+
# value is extends from AbstractGenerator.
|
74
|
+
# * <tt>:fk_limit</tt>: A Hash of foreign key limits, where each key is the name of column and the value is
|
75
|
+
# a number. For example options[:fk_limit][:date_id] = 1000 would limit the foreign key values to something between
|
76
|
+
# 1 and 1000, inclusive.
|
77
|
+
def build_fact(name, options={})
|
78
|
+
options[:rows] ||= 100
|
79
|
+
options[:generators] ||= {}
|
80
|
+
options[:fk_limit] ||= {}
|
81
|
+
rows = []
|
82
|
+
|
83
|
+
fact_class = Fact.to_fact(name)
|
84
|
+
options[:rows].times do
|
85
|
+
row = {}
|
86
|
+
fact_class.content_columns.each do |column|
|
87
|
+
generator = (options[:generators][column.klass] || @generators[column.klass])
|
88
|
+
row[column.name] = generator.generate(column, options)
|
89
|
+
end
|
90
|
+
fact_class.foreign_key_columns.each do |column|
|
91
|
+
fk_limit = (options[:fk_limit][column.name] || 100) - 1
|
92
|
+
row[column.name] = rand(fk_limit) + 1
|
93
|
+
end
|
94
|
+
rows << row
|
95
|
+
end
|
9
96
|
|
97
|
+
rows
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Implement this class to provide an generator implementation for a specific class.
|
102
|
+
class AbstractGenerator
|
103
|
+
# Generate the next value. The column parameter must be an ActiveRecord::Adapter::Column instance.
|
104
|
+
# The options hash is implementation dependent.
|
105
|
+
def generate(column, options={})
|
106
|
+
raise "generate method must be implemented by a subclass"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Basic Date generator
|
111
|
+
class DateGenerator < AbstractGenerator
|
112
|
+
# Generate a random date value
|
113
|
+
#
|
114
|
+
# Options:
|
115
|
+
# *<tt>:start_date</tt>: The start date as a Date or Time object (default 1 year ago)
|
116
|
+
# *<tt>:end_date</tt>: The end date as a Date or Time object (default now)
|
117
|
+
def generate(column, options={})
|
118
|
+
end_date = (options[:end_date] || Time.now).to_date
|
119
|
+
start_date = (options[:start_date] || 1.year.ago).to_date
|
120
|
+
number_of_days = end_date - start_date
|
121
|
+
start_date + rand(number_of_days)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Basic Time generator
|
126
|
+
#
|
127
|
+
# Options:
|
128
|
+
# *<tt>:start_date</tt>: The start date as a Date or Time object (default 1 year ago)
|
129
|
+
# *<tt>:end_date</tt>: The end date as a Date or Time object (default now)
|
130
|
+
class TimeGenerator < DateGenerator #:nodoc:
|
131
|
+
# Generate a random Time value
|
132
|
+
def generate(column, options={})
|
133
|
+
super(column, options).to_time
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Basic Fixnum generator
|
138
|
+
class FixnumGenerator
|
139
|
+
# Generate an integer from 0 to options[:max] inclusive
|
140
|
+
#
|
141
|
+
# Options:
|
142
|
+
# *<tt>:max</tt>: The maximum allowed value (default 1000)
|
143
|
+
# *<tt>:min</tt>: The minimum allowed value (default 0)
|
144
|
+
def generate(column, options={})
|
145
|
+
options[:max] ||= 1000
|
146
|
+
options[:min] ||= 0
|
147
|
+
rand(options[:max] + (-options[:min])) - options[:min]
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Basic Float generator
|
152
|
+
class FloatGenerator
|
153
|
+
# Generate a float from 0 to options[:max] inclusive (default 1000)
|
154
|
+
#
|
155
|
+
# Options:
|
156
|
+
# *<tt>:max</tt>: The maximum allowed value (default 1000)
|
157
|
+
def generate(column, options={})
|
158
|
+
options[:max] ||= 1000
|
159
|
+
rand * options[:max].to_f
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Basic BigDecimal generator
|
164
|
+
class BigDecimalGenerator
|
165
|
+
# Generate a big decimal from 0 to options[:max] inclusive (default 1000)
|
166
|
+
#
|
167
|
+
# Options:
|
168
|
+
# *<tt>:max</tt>: The maximum allowed value (default 1000)
|
169
|
+
def generate(column, options={})
|
170
|
+
options[:max] ||= 1000
|
171
|
+
BigDecimal.new((rand * options[:max].to_f).to_s) # TODO: need BigDecimal type?
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# A basic String generator
|
176
|
+
class StringGenerator
|
177
|
+
# Initialize the StringGenerator.
|
178
|
+
#
|
179
|
+
# Options:
|
180
|
+
# * <tt>:values</tt>: List of possible values
|
181
|
+
# * <tt>:chars</tt>: List of chars to use to generate random values
|
182
|
+
def initialize(options={})
|
183
|
+
@options = options
|
184
|
+
end
|
185
|
+
# Generate a random string
|
186
|
+
#
|
187
|
+
# Options:
|
188
|
+
# * <tt>:values</tt>: An array of values to use. If not specified then random char values will be used.
|
189
|
+
# * <tt>:chars</tt>: An array of characters to use to generate random values (default [a..zA..Z])
|
190
|
+
def generate(column, options={})
|
191
|
+
options[:values] ||= @options[:values]
|
192
|
+
options[:chars] ||= @options[:chars]
|
193
|
+
if options[:values]
|
194
|
+
options[:values][rand(options[:values].length)]
|
195
|
+
else
|
196
|
+
s = ''
|
197
|
+
chars = (options[:chars] || ('a'..'z').to_a + ('A'..'Z').to_a)
|
198
|
+
0.upto(column.limit - 1) do |n|
|
199
|
+
s << chars[rand(chars.length)]
|
200
|
+
end
|
201
|
+
s
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# A basic Boolean generator
|
207
|
+
class BooleanGenerator
|
208
|
+
# Generate a random boolean
|
209
|
+
def generate(column, options={})
|
210
|
+
rand(1) == 1
|
10
211
|
end
|
11
212
|
end
|
12
213
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# Provides 1.1.6 compatibility
|
2
|
+
module ActiveRecord
|
3
|
+
module Calculations
|
4
|
+
module ClassMethods
|
5
|
+
protected
|
6
|
+
def construct_count_options_from_legacy_args(*args)
|
7
|
+
options = {}
|
8
|
+
column_name = :all
|
9
|
+
|
10
|
+
# We need to handle
|
11
|
+
# count()
|
12
|
+
# count(options={})
|
13
|
+
# count(column_name=:all, options={})
|
14
|
+
# count(conditions=nil, joins=nil) # deprecated
|
15
|
+
if args.size > 2
|
16
|
+
raise ArgumentError, "Unexpected parameters passed to count(options={}): #{args.inspect}"
|
17
|
+
elsif args.size > 0
|
18
|
+
if args[0].is_a?(Hash)
|
19
|
+
options = args[0]
|
20
|
+
elsif args[1].is_a?(Hash)
|
21
|
+
column_name, options = args
|
22
|
+
else
|
23
|
+
# Deprecated count(conditions, joins=nil)
|
24
|
+
ActiveSupport::Deprecation.warn(
|
25
|
+
"You called count(#{args[0].inspect}, #{args[1].inspect}), which is a deprecated API call. " +
|
26
|
+
"Instead you should use count(column_name, options). Passing the conditions and joins as " +
|
27
|
+
"string parameters will be removed in Rails 2.0.", caller(2)
|
28
|
+
)
|
29
|
+
options.merge!(:conditions => args[0])
|
30
|
+
options.merge!(:joins => args[1]) if args[1]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
[column_name, options]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Module
|
41
|
+
def alias_method_chain(target, feature)
|
42
|
+
# Strip out punctuation on predicates or bang methods since
|
43
|
+
# e.g. target?_without_feature is not a valid method name.
|
44
|
+
aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
|
45
|
+
yield(aliased_target, punctuation) if block_given?
|
46
|
+
alias_method "#{aliased_target}_without_#{feature}#{punctuation}", target
|
47
|
+
alias_method target, "#{aliased_target}_with_#{feature}#{punctuation}"
|
48
|
+
end
|
49
|
+
end
|
@@ -20,12 +20,14 @@ module ActiveWarehouse
|
|
20
20
|
dimensions << dimension
|
21
21
|
end
|
22
22
|
end
|
23
|
+
alias :pivot_on :pivots_on
|
23
24
|
|
24
25
|
# Defines the fact that this cube reports on
|
25
26
|
def reports_on(fact)
|
26
27
|
# TODO: Validate if one or more dimension is set
|
27
28
|
@fact = fact
|
28
29
|
end
|
30
|
+
alias :report_on :reports_on
|
29
31
|
|
30
32
|
# Rebuild all aggregate classes. Set :force => true to force the rebuild of aggregate classes.
|
31
33
|
def rebuild(options={})
|
@@ -163,30 +165,56 @@ module ActiveWarehouse
|
|
163
165
|
def aggregate_map(column_dimension, column_hierarchy, row_dimension, row_hierarchy, cstage=0, rstage=0)
|
164
166
|
# Fill known cells
|
165
167
|
agg_map = AggregateMap.new
|
166
|
-
agg_records =
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
168
|
+
agg_records = nil
|
169
|
+
# s = Benchmark.realtime do
|
170
|
+
agg_records = aggregate_records(column_dimension, column_hierarchy, cstage, row_dimension, row_hierarchy, rstage)
|
171
|
+
#end
|
172
|
+
# cs = 0
|
173
|
+
# as = 0
|
174
|
+
# calc = 0
|
175
|
+
# x = 0
|
176
|
+
calculated_fields = self.class.fact_class.calculated_fields
|
177
|
+
calculated_field_options = self.class.fact_class.calculated_field_options
|
178
|
+
#puts "loading aggregate_records took #{s}s"
|
179
|
+
#s = Benchmark.realtime do
|
180
|
+
#puts "there are #{agg_records.length} agg_records"
|
181
|
+
#puts "there are #{self.class.fact_class.calculated_fields.length} calculated fields in class #{self.class.fact_class}"
|
182
|
+
agg_records.each do |agg_record|
|
183
|
+
# agg_record is an instance of Aggregate
|
184
|
+
# collect the aggregate record data fields into an array
|
185
|
+
data_array = nil
|
186
|
+
#cs += Benchmark.realtime do
|
187
|
+
data_array = agg_record.data_fields.collect{ |data_field_name| agg_record.send(data_field_name.to_sym) }
|
188
|
+
#end
|
171
189
|
|
172
|
-
|
173
|
-
|
190
|
+
# convert to an average where necessary
|
191
|
+
# TODO: implement
|
174
192
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
193
|
+
# add calculated fields to the data array
|
194
|
+
#calc += Benchmark.realtime do
|
195
|
+
calculated_fields.each do |calculated_field|
|
196
|
+
options = calculated_field_options[calculated_field]
|
197
|
+
data_array << options[:block].call(agg_record)
|
198
|
+
end
|
199
|
+
#end
|
200
|
+
|
201
|
+
# add the data array to the aggregate map
|
202
|
+
#as += Benchmark.realtime do
|
203
|
+
agg_map.add_data(agg_record.dimension2_path, agg_record.dimension1_path, data_array)
|
204
|
+
#end
|
179
205
|
end
|
180
206
|
|
181
|
-
|
182
|
-
|
183
|
-
|
207
|
+
#end
|
208
|
+
#puts "creating the agg_map took #{s}s"
|
209
|
+
#puts "total time spent collecting the data: #{cs}s, avg:#{cs/agg_records.length}s (#{(cs/s) * 100}%)"
|
210
|
+
#puts "total time spent adding the data: #{as}s, avg:#{as/agg_records.length}s (#{(as/s) * 100}%)"
|
211
|
+
#puts "total time spent calculating fields: #{calc}s, avg:#{calc/agg_records.length}s (#{(calc/s) * 100}%)"
|
184
212
|
agg_map
|
185
213
|
end
|
186
214
|
|
187
215
|
protected
|
188
216
|
# Return all of the Aggregate records for the specified dimensions and hierarchies
|
189
|
-
def aggregate_records(column_dimension, column_hierarchy, row_dimension, row_hierarchy)
|
217
|
+
def aggregate_records(column_dimension, column_hierarchy, cstage, row_dimension, row_hierarchy, rstage)
|
190
218
|
k = Aggregate.key(column_dimension, column_hierarchy, row_dimension, row_hierarchy)
|
191
219
|
if aggregates[k].nil?
|
192
220
|
self.class.logger.debug("Aggregate #{k} not found in cache")
|
@@ -217,8 +245,10 @@ module ActiveWarehouse
|
|
217
245
|
aggregate_class = self.class.aggregates[aggregate_meta_data.id]
|
218
246
|
raise "Cannot find aggregate for id #{aggregate_meta_data.id}" if aggregate_class.nil?
|
219
247
|
end
|
220
|
-
|
221
|
-
aggregates[k] = aggregate_class.find(:all
|
248
|
+
|
249
|
+
aggregates[k] = aggregate_class.find(:all,
|
250
|
+
:conditions => ['(dimension1_stage = ? and dimension2_stage = ?) or (dimension1_stage = ? and dimension2_stage = ?)',
|
251
|
+
cstage, rstage, rstage, cstage])
|
222
252
|
end
|
223
253
|
aggregates[k]
|
224
254
|
end
|
@@ -0,0 +1,296 @@
|
|
1
|
+
# require all of the "acts_as" mixins first
|
2
|
+
require 'active_warehouse/dimension/hierarchical_dimension'
|
3
|
+
require 'active_warehouse/dimension/slowly_changing_dimension'
|
4
|
+
|
5
|
+
module ActiveWarehouse #:nodoc
|
6
|
+
# Dimension tables contain the textual descriptors of the business. Dimensions provide the filters which
|
7
|
+
# are applied to facts. Dimensions are the primary source of query constraints, groupings and report
|
8
|
+
# labels.
|
9
|
+
#
|
10
|
+
class Dimension < ActiveRecord::Base
|
11
|
+
include ActiveWarehouse::HierarchicalDimension
|
12
|
+
include ActiveWarehouse::SlowlyChangingDimension
|
13
|
+
|
14
|
+
after_save :expire_value_tree_cache
|
15
|
+
|
16
|
+
class << self
|
17
|
+
# Alternate order by, to be used rather than the current level being queried
|
18
|
+
attr_accessor :order
|
19
|
+
|
20
|
+
# Map of level names to alternate order columns
|
21
|
+
attr_reader :level_orders
|
22
|
+
|
23
|
+
# Define a column to order by. If this value is specified then it will be used rather than the actual
|
24
|
+
# level being queried in the following method calls:
|
25
|
+
# * available_values
|
26
|
+
# * available_child_values
|
27
|
+
# * available_values_tree
|
28
|
+
def set_order(name)
|
29
|
+
@order = name
|
30
|
+
end
|
31
|
+
|
32
|
+
# Define a column to order by for a specific level.
|
33
|
+
def set_level_order(level, name)
|
34
|
+
level_orders[level] = name
|
35
|
+
end
|
36
|
+
|
37
|
+
# Get the level orders map
|
38
|
+
def level_orders
|
39
|
+
@level_orders ||= {}
|
40
|
+
end
|
41
|
+
|
42
|
+
# Define a named attribute hierarchy in the dimension.
|
43
|
+
#
|
44
|
+
# Example: define_hierarchy(:fiscal_calendar, [:fiscal_year, :fiscal_quarter, :fiscal_month])
|
45
|
+
#
|
46
|
+
# This would indicate that one of the drill down paths for this dimension is:
|
47
|
+
# Fiscal Year -> Fiscal Quarter -> Fiscal Month
|
48
|
+
#
|
49
|
+
# Internally the hierarchies are stored in order. The first hierarchy defined will be used as the default
|
50
|
+
# if no hierarchy is specified when rendering a cube.
|
51
|
+
def define_hierarchy(name, levels)
|
52
|
+
hierarchies << name
|
53
|
+
hierarchy_levels[name] = levels
|
54
|
+
end
|
55
|
+
|
56
|
+
# Get the named attribute hierarchy. Returns an array of column names.
|
57
|
+
#
|
58
|
+
# Example: hierarchy(:fiscal_calendar) might return [:fiscal_year, :fiscal_quarter, :fiscal_month]
|
59
|
+
def hierarchy(name)
|
60
|
+
hierarchy_levels[name]
|
61
|
+
end
|
62
|
+
|
63
|
+
# Get the ordered hierarchy names
|
64
|
+
def hierarchies
|
65
|
+
@hierarchies ||= []
|
66
|
+
end
|
67
|
+
|
68
|
+
# Get the hierarchy levels hash
|
69
|
+
def hierarchy_levels
|
70
|
+
@hierarchy_levels ||= {}
|
71
|
+
end
|
72
|
+
|
73
|
+
# Return a symbol used when referring to this dimension. The symbol is calculated by demodulizing and underscoring the
|
74
|
+
# dimension's class name and then removing the trailing _dimension.
|
75
|
+
#
|
76
|
+
# Example: DateDimension will return a symbol :date
|
77
|
+
def sym
|
78
|
+
self.name.demodulize.underscore.gsub(/_dimension/, '').to_sym
|
79
|
+
end
|
80
|
+
|
81
|
+
# Get the table name. By default the table name will be the name of the dimension in singular form.
|
82
|
+
#
|
83
|
+
# Example: DateDimension will have a table called date_dimension
|
84
|
+
def table_name
|
85
|
+
name = self.name.demodulize.underscore
|
86
|
+
set_table_name(name)
|
87
|
+
name
|
88
|
+
end
|
89
|
+
|
90
|
+
# Convert the given name into a dimension class name
|
91
|
+
def class_name(name)
|
92
|
+
dimension_name = name.to_s
|
93
|
+
dimension_name = "#{dimension_name}_dimension" unless dimension_name =~ /_dimension$/
|
94
|
+
dimension_name.classify
|
95
|
+
end
|
96
|
+
|
97
|
+
# Get a class for the specified named dimension
|
98
|
+
def class_for_name(name)
|
99
|
+
class_name(name).constantize
|
100
|
+
end
|
101
|
+
|
102
|
+
# Return the time when the underlying dimension source file was last modified. This is used
|
103
|
+
# to determine if a cube structure rebuild is required
|
104
|
+
def last_modified
|
105
|
+
File.new(__FILE__).mtime
|
106
|
+
end
|
107
|
+
|
108
|
+
# Get the dimension class for the specified dimension parameter. The dimension parameter may be a class,
|
109
|
+
# String or Symbol.
|
110
|
+
def to_dimension(dimension)
|
111
|
+
return dimension if dimension.is_a?(Class) and dimension.superclass == Dimension
|
112
|
+
return class_for_name(dimension)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns a hash of all of the values at the specified hierarchy level mapped to the count at that level.
|
116
|
+
# For example, given a date dimension with years from 2002 to 2004 and a hierarchy defined with:
|
117
|
+
#
|
118
|
+
# hierarchy :cy, [:calendar_year, :calendar_quarter, :calendar_month_name]
|
119
|
+
#
|
120
|
+
# ...then...
|
121
|
+
#
|
122
|
+
# DateDimension.denominator_count(:cy, :calendar_year, :calendar_quarter) returns {'2002' => 4, '2003' => 4, '2004' => 4}
|
123
|
+
#
|
124
|
+
# If the denominator_level parameter is omitted or nil then:
|
125
|
+
#
|
126
|
+
# DateDimension.denominator_count(:cy, :calendar_year) returns {'2003' => 365, '2003' => 365, '2004' => 366}
|
127
|
+
#
|
128
|
+
def denominator_count(hierarchy_name, level, denominator_level=nil)
|
129
|
+
if hierarchy_levels[hierarchy_name].nil?
|
130
|
+
raise ArgumentError, "The hierarchy '#{hierarchy_name}' does not exist in your dimension #{name}"
|
131
|
+
end
|
132
|
+
|
133
|
+
q = nil
|
134
|
+
# If the denominator_level is specified and it is not the last element in the hierarchy then do a distinct count. If
|
135
|
+
# the denominator level is less than the current level then raise an ArgumentError. In other words, if the current level is
|
136
|
+
# calendar month then passing in calendar year as the denominator level would raise an ArgumentErro.
|
137
|
+
#
|
138
|
+
# If the denominator_level is not specified then assume the finest grain possible (in the context of a date dimension
|
139
|
+
# this would be each day) and use the id to count.
|
140
|
+
if denominator_level && hierarchy_levels[hierarchy_name].last != denominator_level
|
141
|
+
level_index = hierarchy_levels[hierarchy_name].index(level)
|
142
|
+
denominator_index = hierarchy_levels[hierarchy_name].index(denominator_level)
|
143
|
+
|
144
|
+
if level_index.nil?
|
145
|
+
raise ArgumentError, "The level '#{level}' does not appear to exist"
|
146
|
+
end
|
147
|
+
if denominator_index.nil?
|
148
|
+
raise ArgumentError, "The denominator level '#{denominator_level}' does not appear to exist"
|
149
|
+
end
|
150
|
+
if hierarchy_levels[hierarchy_name].index(denominator_level) < hierarchy_levels[hierarchy_name].index(level)
|
151
|
+
raise ArgumentError, "The index of the denominator level '#{denominator_level}' in the hierarchy '#{hierarchy_name}' must be greater than or equal to the level '#{level}'"
|
152
|
+
end
|
153
|
+
|
154
|
+
q = "select #{level} as level, count(distinct(#{denominator_level})) as level_count from #{table_name} group by #{level}"
|
155
|
+
else
|
156
|
+
q = "select #{level} as level, count(id) as level_count from #{table_name} group by #{level}"
|
157
|
+
end
|
158
|
+
denominators = {}
|
159
|
+
# TODO: fix to use select_all instead of execute
|
160
|
+
connection.select_all(q).each do |row|
|
161
|
+
denominators[row['level']] = row['level_count'].to_i
|
162
|
+
end
|
163
|
+
denominators
|
164
|
+
end
|
165
|
+
|
166
|
+
# Get the foreign key for this dimension which is used in Fact tables.
|
167
|
+
#
|
168
|
+
# Example: DateDimension would have a foreign key of date_id
|
169
|
+
def foreign_key
|
170
|
+
table_name.sub(/_dimension/,'') + '_id'
|
171
|
+
end
|
172
|
+
|
173
|
+
# Get an array of the available values for a particular hierarchy level
|
174
|
+
# For example, given a DateDimension with data from 2002 to 2004:
|
175
|
+
#
|
176
|
+
# available_values('calendar_year') returns ['2002','2003','2004']
|
177
|
+
def available_values(level)
|
178
|
+
level_method = level.to_sym
|
179
|
+
level = connection.quote_column_name(level.to_s)
|
180
|
+
order = level_orders[level] || self.order || level
|
181
|
+
find(:all, :select => level, :group => level, :order => order).collect {|dim| dim.send(level_method)}
|
182
|
+
end
|
183
|
+
|
184
|
+
# Get an array of child values for a particular parent in the hierachy
|
185
|
+
# For example, given a DateDimension with data from 2002 to 2004:
|
186
|
+
#
|
187
|
+
# available_child_values(:cy, [2002, 'Q1']) returns ['January', 'Feburary', 'March', 'April']
|
188
|
+
def available_child_values(hierarchy_name, parent_values)
|
189
|
+
if hierarchy_levels[hierarchy_name].nil?
|
190
|
+
raise ArgumentError, "The hierarchy '#{hierarchy_name}' does not exist in your dimension #{name}"
|
191
|
+
end
|
192
|
+
|
193
|
+
levels = hierarchy_levels[hierarchy_name]
|
194
|
+
if levels.length <= parent_values.length
|
195
|
+
raise ArgumentError, "The parent_values '#{parent_values.to_yaml}' exceeds the hierarchy depth #{levels.to_yaml}"
|
196
|
+
end
|
197
|
+
|
198
|
+
child_level = levels[parent_values.length].to_s
|
199
|
+
|
200
|
+
# Create the conditions array. Will work with 1.1.6.
|
201
|
+
conditions_parts = []
|
202
|
+
conditions_values = []
|
203
|
+
parent_values.each_with_index do |value, index|
|
204
|
+
conditions_parts << "#{levels[index]} = ?"
|
205
|
+
conditions_values << value
|
206
|
+
end
|
207
|
+
conditions = [conditions_parts.join(' AND ')] + conditions_values unless conditions_parts.empty?
|
208
|
+
|
209
|
+
child_level_method = child_level.to_sym
|
210
|
+
child_level = connection.quote_column_name(child_level)
|
211
|
+
order = level_orders[child_level] || self.order || child_level
|
212
|
+
|
213
|
+
options = {:select => child_level, :group => child_level, :order => order}
|
214
|
+
options[:conditions] = conditions unless conditions.nil?
|
215
|
+
find(:all, options).collect {|dim| dim.send(child_level_method)}
|
216
|
+
end
|
217
|
+
alias :available_children_values :available_child_values
|
218
|
+
|
219
|
+
# Get a tree of Node objects for all of the values in the specified hierarchy.
|
220
|
+
def available_values_tree(hierarchy_name)
|
221
|
+
root = value_tree_cache[hierarchy_name]
|
222
|
+
if root.nil?
|
223
|
+
root = Node.new('All', '__ROOT__')
|
224
|
+
levels = hierarchy(hierarchy_name)
|
225
|
+
nodes = {nil => root}
|
226
|
+
level_list = levels.collect{|level| connection.quote_column_name(level) }.join(',')
|
227
|
+
order = self.order || level_list
|
228
|
+
find(:all, :select => level_list, :group => level_list, :order => order).each do |dim|
|
229
|
+
parent_node = root
|
230
|
+
levels.each do |level|
|
231
|
+
node_value = dim.send(level)
|
232
|
+
child_node = parent_node.optionally_add_child(node_value, level)
|
233
|
+
parent_node = child_node
|
234
|
+
end
|
235
|
+
end
|
236
|
+
value_tree_cache[hierarchy_name] = root
|
237
|
+
end
|
238
|
+
root
|
239
|
+
end
|
240
|
+
|
241
|
+
protected
|
242
|
+
# Get the value tree cache
|
243
|
+
def value_tree_cache
|
244
|
+
@value_tree_cache ||= {}
|
245
|
+
end
|
246
|
+
|
247
|
+
class Node#:nodoc:
|
248
|
+
attr_reader :value, :children, :parent, :level
|
249
|
+
|
250
|
+
def initialize(value, level, parent = nil)
|
251
|
+
@children = []
|
252
|
+
@value = value
|
253
|
+
@parent = parent
|
254
|
+
@level = level
|
255
|
+
end
|
256
|
+
|
257
|
+
def has_child?(child_value)
|
258
|
+
!self.child(child_value).nil?
|
259
|
+
end
|
260
|
+
|
261
|
+
def child(child_value)
|
262
|
+
@children.each do |c|
|
263
|
+
return c if c.value == child_value
|
264
|
+
end
|
265
|
+
nil
|
266
|
+
end
|
267
|
+
|
268
|
+
def add_child(child_value, level)
|
269
|
+
child = Node.new(child_value, level, self)
|
270
|
+
@children << child
|
271
|
+
child
|
272
|
+
end
|
273
|
+
|
274
|
+
def optionally_add_child(child_value, level)
|
275
|
+
c = child(child_value)
|
276
|
+
c = add_child(child_value, level) unless c
|
277
|
+
c
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
public
|
282
|
+
# Expire the value tree cache. This should be called if the dimension
|
283
|
+
def expire_value_tree_cache
|
284
|
+
@value_tree_cache = nil
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
public
|
289
|
+
def expire_value_tree_cache
|
290
|
+
self.class.expire_value_tree_cache
|
291
|
+
end
|
292
|
+
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
require 'active_warehouse/dimension/bridge'
|