activewarehouse 0.1.0 → 0.2.0
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.
- 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'
|