mainej-activewarehouse 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/activewarehouse/README +99 -0
- data/activewarehouse/Rakefile +165 -0
- data/activewarehouse/TODO +4 -0
- data/activewarehouse/db/migrations/001_create_table_reports.rb +28 -0
- data/activewarehouse/doc/references.txt +4 -0
- data/activewarehouse/generators/bridge/USAGE +1 -0
- data/activewarehouse/generators/bridge/bridge_generator.rb +46 -0
- data/activewarehouse/generators/bridge/templates/fixture.yml +5 -0
- data/activewarehouse/generators/bridge/templates/migration.rb +27 -0
- data/activewarehouse/generators/bridge/templates/model.rb +3 -0
- data/activewarehouse/generators/bridge/templates/unit_test.rb +8 -0
- data/activewarehouse/generators/cube/USAGE +1 -0
- data/activewarehouse/generators/cube/cube_generator.rb +28 -0
- data/activewarehouse/generators/cube/templates/model.rb +3 -0
- data/activewarehouse/generators/cube/templates/unit_test.rb +8 -0
- data/activewarehouse/generators/date_dimension/USAGE +1 -0
- data/activewarehouse/generators/date_dimension/date_dimension_generator.rb +16 -0
- data/activewarehouse/generators/date_dimension/templates/fixture.yml +5 -0
- data/activewarehouse/generators/date_dimension/templates/migration.rb +31 -0
- data/activewarehouse/generators/date_dimension/templates/model.rb +3 -0
- data/activewarehouse/generators/date_dimension/templates/unit_test.rb +8 -0
- data/activewarehouse/generators/dimension/USAGE +1 -0
- data/activewarehouse/generators/dimension/dimension_generator.rb +46 -0
- data/activewarehouse/generators/dimension/templates/fixture.yml +5 -0
- data/activewarehouse/generators/dimension/templates/migration.rb +11 -0
- data/activewarehouse/generators/dimension/templates/model.rb +3 -0
- data/activewarehouse/generators/dimension/templates/unit_test.rb +8 -0
- data/activewarehouse/generators/dimension_view/USAGE +1 -0
- data/activewarehouse/generators/dimension_view/dimension_view_generator.rb +62 -0
- data/activewarehouse/generators/dimension_view/templates/migration.rb +17 -0
- data/activewarehouse/generators/dimension_view/templates/model.rb +3 -0
- data/activewarehouse/generators/dimension_view/templates/unit_test.rb +10 -0
- data/activewarehouse/generators/fact/USAGE +1 -0
- data/activewarehouse/generators/fact/fact_generator.rb +46 -0
- data/activewarehouse/generators/fact/templates/fixture.yml +5 -0
- data/activewarehouse/generators/fact/templates/migration.rb +13 -0
- data/activewarehouse/generators/fact/templates/model.rb +3 -0
- data/activewarehouse/generators/fact/templates/unit_test.rb +10 -0
- data/activewarehouse/generators/time_dimension/USAGE +1 -0
- data/activewarehouse/generators/time_dimension/templates/fixture.yml +5 -0
- data/activewarehouse/generators/time_dimension/templates/migration.rb +12 -0
- data/activewarehouse/generators/time_dimension/templates/model.rb +3 -0
- data/activewarehouse/generators/time_dimension/templates/unit_test.rb +8 -0
- data/activewarehouse/generators/time_dimension/time_dimension_generator.rb +14 -0
- data/activewarehouse/init.rb +1 -0
- data/activewarehouse/install.rb +5 -0
- data/activewarehouse/lib/active_warehouse.rb +91 -0
- data/activewarehouse/lib/active_warehouse/aggregate.rb +75 -0
- data/activewarehouse/lib/active_warehouse/aggregate/dwarf_aggregate.rb +369 -0
- data/activewarehouse/lib/active_warehouse/aggregate/dwarf_common.rb +44 -0
- data/activewarehouse/lib/active_warehouse/aggregate/dwarf_printer.rb +34 -0
- data/activewarehouse/lib/active_warehouse/aggregate/no_aggregate.rb +212 -0
- data/activewarehouse/lib/active_warehouse/aggregate/pid_aggregate.rb +29 -0
- data/activewarehouse/lib/active_warehouse/aggregate_field.rb +59 -0
- data/activewarehouse/lib/active_warehouse/bridge.rb +19 -0
- data/activewarehouse/lib/active_warehouse/bridge/hierarchy_bridge.rb +46 -0
- data/activewarehouse/lib/active_warehouse/builder.rb +3 -0
- data/activewarehouse/lib/active_warehouse/builder/date_dimension_builder.rb +91 -0
- data/activewarehouse/lib/active_warehouse/builder/generator/generator.rb +13 -0
- data/activewarehouse/lib/active_warehouse/builder/generator/name_generator.rb +20 -0
- data/activewarehouse/lib/active_warehouse/builder/generator/paragraph_generator.rb +11 -0
- data/activewarehouse/lib/active_warehouse/builder/random_data_builder.rb +239 -0
- data/activewarehouse/lib/active_warehouse/builder/test_data_builder.rb +54 -0
- data/activewarehouse/lib/active_warehouse/calculated_field.rb +27 -0
- data/activewarehouse/lib/active_warehouse/compat/compat.rb +49 -0
- data/activewarehouse/lib/active_warehouse/core_ext.rb +1 -0
- data/activewarehouse/lib/active_warehouse/core_ext/time.rb +5 -0
- data/activewarehouse/lib/active_warehouse/core_ext/time/calculations.rb +40 -0
- data/activewarehouse/lib/active_warehouse/cube.rb +235 -0
- data/activewarehouse/lib/active_warehouse/cube_query_result.rb +69 -0
- data/activewarehouse/lib/active_warehouse/dimension.rb +329 -0
- data/activewarehouse/lib/active_warehouse/dimension/date_dimension.rb +15 -0
- data/activewarehouse/lib/active_warehouse/dimension/dimension_reflection.rb +21 -0
- data/activewarehouse/lib/active_warehouse/dimension/dimension_view.rb +27 -0
- data/activewarehouse/lib/active_warehouse/dimension/hierarchical_dimension.rb +99 -0
- data/activewarehouse/lib/active_warehouse/dimension/slowly_changing_dimension.rb +147 -0
- data/activewarehouse/lib/active_warehouse/fact.rb +239 -0
- data/activewarehouse/lib/active_warehouse/field.rb +74 -0
- data/activewarehouse/lib/active_warehouse/migrations.rb +64 -0
- data/activewarehouse/lib/active_warehouse/ordered_hash.rb +34 -0
- data/activewarehouse/lib/active_warehouse/prejoin_fact.rb +97 -0
- data/activewarehouse/lib/active_warehouse/report.rb +7 -0
- data/activewarehouse/lib/active_warehouse/report/abstract_report.rb +149 -0
- data/activewarehouse/lib/active_warehouse/report/chart_report.rb +9 -0
- data/activewarehouse/lib/active_warehouse/report/data_cell.rb +21 -0
- data/activewarehouse/lib/active_warehouse/report/data_column.rb +19 -0
- data/activewarehouse/lib/active_warehouse/report/data_row.rb +15 -0
- data/activewarehouse/lib/active_warehouse/report/dimension.rb +58 -0
- data/activewarehouse/lib/active_warehouse/report/table_report.rb +38 -0
- data/activewarehouse/lib/active_warehouse/version.rb +9 -0
- data/activewarehouse/lib/active_warehouse/view.rb +9 -0
- data/activewarehouse/lib/active_warehouse/view/crumb.rb +64 -0
- data/activewarehouse/lib/active_warehouse/view/report_helper.rb +98 -0
- data/activewarehouse/lib/active_warehouse/view/table_view.rb +134 -0
- data/activewarehouse/lib/active_warehouse/view/yui_adapter.rb +68 -0
- data/activewarehouse/tasks/active_warehouse_tasks.rake +122 -0
- metadata +237 -0
@@ -0,0 +1,235 @@
|
|
1
|
+
module ActiveWarehouse
|
2
|
+
# A Cube represents a collection of dimensions operating on a fact. The Cube
|
3
|
+
# provides a front-end for getting at the
|
4
|
+
# underlying data. Cubes support pluggable aggregation. The default aggregation
|
5
|
+
# is the NoAggregate which goes directly
|
6
|
+
# to the fact and dimensions to answer queries.
|
7
|
+
class Cube
|
8
|
+
class << self
|
9
|
+
|
10
|
+
# Callback which is invoked when subclasses are created
|
11
|
+
def inherited(subclass)
|
12
|
+
subclasses << subclass
|
13
|
+
end
|
14
|
+
|
15
|
+
# Get a list of all known subclasses
|
16
|
+
def subclasses
|
17
|
+
@subclasses ||= []
|
18
|
+
end
|
19
|
+
|
20
|
+
# Defines the dimensions that this cube pivots on. If the fact name and
|
21
|
+
# cube name are different (for example, if a PurchaseCube does not report
|
22
|
+
# on a PurchaseFact) then you *must* declare the <code>reports_on</code>
|
23
|
+
# first.
|
24
|
+
def pivots_on(*dimension_list)
|
25
|
+
@dimensions_hierarchies = OrderedHash.new
|
26
|
+
@dimensions = []
|
27
|
+
dimension_list.each do |dimension|
|
28
|
+
case dimension
|
29
|
+
when Symbol, String
|
30
|
+
dimensions << dimension.to_sym
|
31
|
+
dimensions_hierarchies[dimension.to_sym] = fact_class.dimension_class(dimension).hierarchies
|
32
|
+
when Hash
|
33
|
+
dimension_name = dimension.keys.first.to_sym
|
34
|
+
dimensions << dimension_name
|
35
|
+
dimensions_hierarchies[dimension_name] = [dimension[dimension_name]].flatten
|
36
|
+
else
|
37
|
+
raise ArgumentError, "Each argument to pivot_on must be a symbol, string or Hash"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
alias :pivot_on :pivots_on
|
42
|
+
|
43
|
+
# Defines the fact name, without the 'Fact' suffix, that this cube
|
44
|
+
# reports on. For instance, if you have PurchaseFact, you could then
|
45
|
+
# call <code>reports_on :purchase</code>.
|
46
|
+
#
|
47
|
+
# The default value for reports_on is to take the name of the cube,
|
48
|
+
# i.e. PurchaseCube, and remove the Cube suffix. The assumption is that
|
49
|
+
# your Cube name matches your Fact name.
|
50
|
+
def reports_on(fact_name)
|
51
|
+
@fact_name = fact_name
|
52
|
+
end
|
53
|
+
alias :report_on :reports_on
|
54
|
+
|
55
|
+
# Rebuild the data warehouse.
|
56
|
+
def rebuild(options={})
|
57
|
+
populate(options)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Populate the data warehouse. Delegate to aggregate.populate
|
61
|
+
def populate(options={})
|
62
|
+
aggregate.populate
|
63
|
+
end
|
64
|
+
|
65
|
+
# Get the dimensions that this cube pivots on
|
66
|
+
def dimensions
|
67
|
+
@dimensions ||= fact_class.dimension_relationships.collect{|k,v| k}
|
68
|
+
end
|
69
|
+
|
70
|
+
# Get an OrderedHash of each dimension mapped to its hierarchies which
|
71
|
+
# will be included in the cube
|
72
|
+
def dimensions_hierarchies
|
73
|
+
if @dimensions_hierarchies.nil?
|
74
|
+
@dimensions_hierarchies = OrderedHash.new
|
75
|
+
dimensions.each do |dimension|
|
76
|
+
@dimensions_hierarchies[dimension] = fact_class.dimension_class(dimension).hierarchies
|
77
|
+
end
|
78
|
+
end
|
79
|
+
@dimensions_hierarchies
|
80
|
+
end
|
81
|
+
|
82
|
+
# returns true if this cube pivots on a hierarchical dimension.
|
83
|
+
def pivot_on_hierarchical_dimension?
|
84
|
+
dimension_classes.each do |dimension|
|
85
|
+
return true if dimension.hierarchical_dimension?
|
86
|
+
end
|
87
|
+
return false
|
88
|
+
end
|
89
|
+
|
90
|
+
# returns the aggregate fields for this cube
|
91
|
+
# removing the aggregate fields that are defined in fact class that are
|
92
|
+
# related to hierarchical dimension, but the cube doesn't pivot on any
|
93
|
+
# hierarchical dimensions
|
94
|
+
# The method also further removes the not appropreate aggregate fields
|
95
|
+
# for the type of dimensions passed in if they exists.
|
96
|
+
def aggregate_fields(dims=[])
|
97
|
+
agg_fields = fact_class.aggregate_fields.reject {|field| !pivot_on_hierarchical_dimension? and !field.levels_from_parent.empty? }
|
98
|
+
dims.each do |dim|
|
99
|
+
if !dim.blank? and fact_class.has_and_belongs_to_many_relationship?(dim.to_sym)
|
100
|
+
return agg_fields.reject {|field| !field.is_count_distinct?}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
agg_fields
|
104
|
+
end
|
105
|
+
|
106
|
+
def calculated_fields
|
107
|
+
fact_class.calculated_fields
|
108
|
+
end
|
109
|
+
|
110
|
+
def fields(dims=[])
|
111
|
+
aggregate_fields(dims) + calculated_fields
|
112
|
+
end
|
113
|
+
|
114
|
+
def field_names(dims=[])
|
115
|
+
fields(dims).map { |field| field.name.to_sym }
|
116
|
+
end
|
117
|
+
|
118
|
+
def fields_for_select(dims=[])
|
119
|
+
fields(dims).inject({}) { |select_hash, field| select_hash.update field.label.to_s => field.name.to_sym }
|
120
|
+
end
|
121
|
+
|
122
|
+
# Get the class name for the specified cube name
|
123
|
+
# Example: Regional Sales will become RegionalSalesCube
|
124
|
+
def class_name(name)
|
125
|
+
cube_name = name.to_s
|
126
|
+
cube_name = "#{cube_name}_cube" unless cube_name =~ /_cube$/
|
127
|
+
cube_name.classify
|
128
|
+
end
|
129
|
+
|
130
|
+
# Get the aggregated fact class name
|
131
|
+
def fact_class_name
|
132
|
+
ActiveWarehouse::Fact.class_name(@fact_name || name.sub(/Cube$/,'').underscore.to_sym)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Get the aggregated fact class instance
|
136
|
+
def fact_class
|
137
|
+
fact_class_name.constantize
|
138
|
+
end
|
139
|
+
|
140
|
+
# Get a list of dimension class instances
|
141
|
+
def dimension_classes
|
142
|
+
dimensions.collect do |dimension_name|
|
143
|
+
dimension_class(dimension_name)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Get the dimension class for the specified dimension name
|
148
|
+
def dimension_class(dimension_name)
|
149
|
+
fact_class.dimension_relationships[dimension_name.to_sym].class_name.constantize
|
150
|
+
end
|
151
|
+
|
152
|
+
# Get the cube logger
|
153
|
+
def logger
|
154
|
+
@logger ||= Logger.new('cube.log')
|
155
|
+
end
|
156
|
+
|
157
|
+
# Get the time when the fact or any dimension referenced in this cube
|
158
|
+
# was last modified
|
159
|
+
def last_modified
|
160
|
+
lm = fact_class.last_modified
|
161
|
+
dimensions.each do |dimension|
|
162
|
+
dim = ActiveWarehouse::Dimension.class_for_name(dimension)
|
163
|
+
lm = dim.last_modified if dim.last_modified > lm
|
164
|
+
end
|
165
|
+
lm
|
166
|
+
end
|
167
|
+
|
168
|
+
# The temp directory for storing files during warehouse rebuilds
|
169
|
+
attr_accessor :temp_dir
|
170
|
+
def temp_dir
|
171
|
+
@temp_dir ||= '/tmp'
|
172
|
+
end
|
173
|
+
|
174
|
+
# Specify the ActiveRecord class to connect through
|
175
|
+
# Note: this is a potential directive in a Cube subclass
|
176
|
+
attr_accessor :connect_through
|
177
|
+
def connect_through
|
178
|
+
@connect_through ||= ActiveRecord::Base
|
179
|
+
end
|
180
|
+
|
181
|
+
# Get an adapter connection
|
182
|
+
def connection
|
183
|
+
connect_through.connection
|
184
|
+
end
|
185
|
+
|
186
|
+
# Defaults to NoAggregate strategy.
|
187
|
+
def aggregate
|
188
|
+
@aggregate ||= ActiveWarehouse::Aggregate::NoAggregate.new(self)
|
189
|
+
end
|
190
|
+
|
191
|
+
def aggregate_class(agg_class)
|
192
|
+
@aggregate = agg_class.new(self)
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
|
197
|
+
public
|
198
|
+
# Query the cube. The column dimension, column hierarchy, row dimension and
|
199
|
+
# row hierarchy are all required.
|
200
|
+
#
|
201
|
+
# The conditions value is a String that represents a SQL condition appended
|
202
|
+
# to the where clause. TODO: this may eventually be converted to another
|
203
|
+
# query language.
|
204
|
+
#
|
205
|
+
# The cstage value represents the current column drill down stage and
|
206
|
+
# defaults to 0.
|
207
|
+
#
|
208
|
+
# The rstage value represents the current row drill down stage and defaults
|
209
|
+
# to 0. Filters contains key/value pairs where the key is a string of
|
210
|
+
# 'dimension.column' and the value is the value to filter by. For example:
|
211
|
+
#
|
212
|
+
# filters = {'date.calendar_year' => 2007, 'product.category' => 'Food'}
|
213
|
+
# query(:date, :cy, :store, :region, 1, 0, filters)
|
214
|
+
#
|
215
|
+
# Note that product.category refers to a dimension which is not actually
|
216
|
+
# visible but which is both part of the cube and is used for filtering.
|
217
|
+
def query(*args)
|
218
|
+
self.class.aggregate.query(*args)
|
219
|
+
end
|
220
|
+
|
221
|
+
# Similar to query, but expects slightly different arguments. See
|
222
|
+
# ActiveWarehouse::Aggregate::Aggregate.query_row_and_column for
|
223
|
+
# details.
|
224
|
+
def query_row_and_column(*args)
|
225
|
+
self.class.aggregate.query_row_and_column(*args)
|
226
|
+
end
|
227
|
+
|
228
|
+
# Get the database connection (delegates to Cube.connection class method)
|
229
|
+
def connection
|
230
|
+
self.class.connection
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
234
|
+
|
235
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module ActiveWarehouse #:nodoc:
|
2
|
+
# Class that holds the results of a Cube query
|
3
|
+
class CubeQueryResult
|
4
|
+
attr_reader :aggregate_fields_hash
|
5
|
+
|
6
|
+
# Initialize the aggregate map with an array of AggregateField instances.
|
7
|
+
# The AggregateFields are used to typecast the raw values coming from
|
8
|
+
# the database. Thank you very little, DBI.
|
9
|
+
def initialize(aggregate_fields)
|
10
|
+
raise ArgumentError, "aggregate_fields must not be empty" unless aggregate_fields && aggregate_fields.size > 0
|
11
|
+
@aggregate_fields_hash = {}
|
12
|
+
aggregate_fields.each {|c| @aggregate_fields_hash[c.label] = c}
|
13
|
+
@values_map = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# Return true if the aggregate map includes the specified row value
|
17
|
+
def has_row_values?(row_value)
|
18
|
+
@values_map.has_key?(row_value.to_s)
|
19
|
+
end
|
20
|
+
|
21
|
+
# iterate through every row and column combination
|
22
|
+
def each
|
23
|
+
@values_map.each do |key, value|
|
24
|
+
yield key, value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def value(row_value, col_value, field_label)
|
29
|
+
#puts "getting value #{row_value},#{col_value},#{field_label}"
|
30
|
+
values(row_value, col_value)[field_label]
|
31
|
+
end
|
32
|
+
|
33
|
+
# returns a hash of type casted fact values for the intersection of
|
34
|
+
# row_value and col_value
|
35
|
+
def values(row_value, col_value)
|
36
|
+
row = @values_map[row_value.to_s]
|
37
|
+
return empty_hash_for_missing_row_or_column if row.nil?
|
38
|
+
facts = row[col_value.to_s]
|
39
|
+
return empty_hash_for_missing_row_or_column if facts.nil?
|
40
|
+
facts
|
41
|
+
end
|
42
|
+
|
43
|
+
# Add a hash of aggregated facts for the given row and column values.
|
44
|
+
# For instance, add_data('Southeast', 2005, {:sales_sum => 40000, :sales_count => 40})
|
45
|
+
# This method will typecast the values in aggregated_facts.
|
46
|
+
def add_data(row_value, col_value, aggregated_facts)
|
47
|
+
#puts "Adding data for #{row_value}, #{col_value} [data=[#{aggregated_facts.join(',')}]]"
|
48
|
+
@values_map[row_value.to_s] ||= {}
|
49
|
+
@values_map[row_value.to_s][col_value.to_s] = typecast_facts(aggregated_facts)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def empty_hash_for_missing_row_or_column
|
54
|
+
empty = {}
|
55
|
+
aggregate_fields_hash.keys.each {|k| empty[k] = 0}
|
56
|
+
empty
|
57
|
+
end
|
58
|
+
|
59
|
+
def typecast_facts(raw_facts)
|
60
|
+
raw_facts.each do |k,v|
|
61
|
+
field = aggregate_fields_hash[k]
|
62
|
+
if field.nil?
|
63
|
+
raise ArgumentError, "'#{k}' is an unknown aggregate field in this query result"
|
64
|
+
end
|
65
|
+
raw_facts[k] = field.type_cast(v)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,329 @@
|
|
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
|
+
require 'active_warehouse/dimension/dimension_reflection'
|
5
|
+
|
6
|
+
ActiveRecord::Reflection::AssociationReflection.send(:include, ActiveWarehouse::DimensionReflection)
|
7
|
+
|
8
|
+
module ActiveWarehouse #:nodoc
|
9
|
+
# Dimension tables contain the textual descriptors of the business. Dimensions
|
10
|
+
# provide the filters which are applied to facts. Dimensions are the primary
|
11
|
+
# source of query constraints, groupings and report labels.
|
12
|
+
class Dimension < ActiveRecord::Base
|
13
|
+
include ActiveWarehouse::HierarchicalDimension
|
14
|
+
include ActiveWarehouse::SlowlyChangingDimension
|
15
|
+
|
16
|
+
after_save :expire_value_tree_cache
|
17
|
+
|
18
|
+
class << self
|
19
|
+
# Alternate order by, to be used rather than the current level being queried
|
20
|
+
attr_accessor :order
|
21
|
+
|
22
|
+
# Map of level names to alternate order columns
|
23
|
+
attr_reader :level_orders
|
24
|
+
|
25
|
+
# Define a column to order by. If this value is specified then it will be
|
26
|
+
# used rather than the actual level being queried in the following method
|
27
|
+
# calls:
|
28
|
+
# * available_values
|
29
|
+
# * available_child_values
|
30
|
+
# * available_values_tree
|
31
|
+
def set_order(name)
|
32
|
+
@order = name
|
33
|
+
end
|
34
|
+
|
35
|
+
# Define a column to order by for a specific level.
|
36
|
+
def set_level_order(level, name)
|
37
|
+
level_orders[level] = name
|
38
|
+
end
|
39
|
+
|
40
|
+
# Get the level orders map
|
41
|
+
def level_orders
|
42
|
+
@level_orders ||= {}
|
43
|
+
end
|
44
|
+
|
45
|
+
# Define a named attribute hierarchy in the dimension.
|
46
|
+
#
|
47
|
+
# Example: define_hierarchy(:fiscal_calendar, [:fiscal_year, :fiscal_quarter, :fiscal_month])
|
48
|
+
#
|
49
|
+
# This would indicate that one of the drill down paths for this dimension is:
|
50
|
+
# Fiscal Year -> Fiscal Quarter -> Fiscal Month
|
51
|
+
#
|
52
|
+
# Internally the hierarchies are stored in order. The first hierarchy
|
53
|
+
# defined will be used as the default if no hierarchy is specified when
|
54
|
+
# rendering a cube.
|
55
|
+
def define_hierarchy(name, levels)
|
56
|
+
hierarchies << name
|
57
|
+
hierarchy_levels[name] = levels
|
58
|
+
end
|
59
|
+
|
60
|
+
# Get the named attribute hierarchy. Returns an array of column names.
|
61
|
+
#
|
62
|
+
# Example: hierarchy(:fiscal_calendar) might return [:fiscal_year, :fiscal_quarter, :fiscal_month]
|
63
|
+
def hierarchy(name)
|
64
|
+
hierarchy_levels[name]
|
65
|
+
end
|
66
|
+
|
67
|
+
# Get the ordered hierarchy names
|
68
|
+
def hierarchies
|
69
|
+
@hierarchies ||= []
|
70
|
+
end
|
71
|
+
|
72
|
+
# Get the hierarchy levels hash
|
73
|
+
def hierarchy_levels
|
74
|
+
@hierarchy_levels ||= {}
|
75
|
+
end
|
76
|
+
|
77
|
+
# Return a symbol used when referring to this dimension. The symbol is
|
78
|
+
# calculated by demodulizing and underscoring the
|
79
|
+
# dimension's class name and then removing the trailing _dimension.
|
80
|
+
#
|
81
|
+
# Example: DateDimension will return a symbol :date
|
82
|
+
def sym
|
83
|
+
self.name.demodulize.underscore.gsub(/_dimension/, '').to_sym
|
84
|
+
end
|
85
|
+
|
86
|
+
# Get the table name. By default the table name will be the name of the
|
87
|
+
# dimension in singular form.
|
88
|
+
#
|
89
|
+
# Example: DateDimension will have a table called date_dimension
|
90
|
+
def table_name
|
91
|
+
name = self.name.demodulize.underscore
|
92
|
+
set_table_name(name)
|
93
|
+
name
|
94
|
+
end
|
95
|
+
|
96
|
+
# Convert the given name into a dimension class name
|
97
|
+
def class_name(name)
|
98
|
+
dimension_name = name.to_s
|
99
|
+
dimension_name = "#{dimension_name}_dimension" unless dimension_name =~ /_dimension$/
|
100
|
+
dimension_name.classify
|
101
|
+
end
|
102
|
+
|
103
|
+
# Get a class for the specified named dimension
|
104
|
+
def class_for_name(name)
|
105
|
+
class_name(name).constantize
|
106
|
+
end
|
107
|
+
|
108
|
+
# Return the time when the underlying dimension source file was last
|
109
|
+
# modified. This is used
|
110
|
+
# to determine if a cube structure rebuild is required
|
111
|
+
def last_modified
|
112
|
+
File.new(__FILE__).mtime
|
113
|
+
end
|
114
|
+
|
115
|
+
# Get the dimension class for the specified dimension parameter. The
|
116
|
+
# dimension parameter may be a class, String or Symbol.
|
117
|
+
def to_dimension(dimension)
|
118
|
+
return dimension if dimension.is_a?(Class) and dimension.ancestors.include?(Dimension)
|
119
|
+
return class_for_name(dimension)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns a hash of all of the values at the specified hierarchy level
|
123
|
+
# mapped to the count at that level. For example, given a date dimension
|
124
|
+
# with years from 2002 to 2004 and a hierarchy defined with:
|
125
|
+
#
|
126
|
+
# hierarchy :cy, [:calendar_year, :calendar_quarter, :calendar_month_name]
|
127
|
+
#
|
128
|
+
# ...then...
|
129
|
+
#
|
130
|
+
# DateDimension.denominator_count(:cy, :calendar_year, :calendar_quarter)
|
131
|
+
# returns {'2002' => 4, '2003' => 4, '2004' => 4}
|
132
|
+
#
|
133
|
+
# If the denominator_level parameter is omitted or nil then:
|
134
|
+
#
|
135
|
+
# DateDimension.denominator_count(:cy, :calendar_year) returns
|
136
|
+
# {'2003' => 365, '2003' => 365, '2004' => 366}
|
137
|
+
#
|
138
|
+
def denominator_count(hierarchy_name, level, denominator_level=nil)
|
139
|
+
if hierarchy_levels[hierarchy_name].nil?
|
140
|
+
raise ArgumentError, "The hierarchy '#{hierarchy_name}' does not exist in your dimension #{name}"
|
141
|
+
end
|
142
|
+
|
143
|
+
q = nil
|
144
|
+
# If the denominator_level is specified and it is not the last element
|
145
|
+
# in the hierarchy then do a distinct count. If
|
146
|
+
# the denominator level is less than the current level then raise an
|
147
|
+
# ArgumentError. In other words, if the current level is
|
148
|
+
# calendar month then passing in calendar year as the denominator level
|
149
|
+
# would raise an ArgumentErro.
|
150
|
+
#
|
151
|
+
# If the denominator_level is not specified then assume the finest grain
|
152
|
+
# possible (in the context of a date dimension this would be each day)
|
153
|
+
# and use the id to count.
|
154
|
+
if denominator_level && hierarchy_levels[hierarchy_name].last != denominator_level
|
155
|
+
level_index = hierarchy_levels[hierarchy_name].index(level)
|
156
|
+
denominator_index = hierarchy_levels[hierarchy_name].index(denominator_level)
|
157
|
+
|
158
|
+
if level_index.nil?
|
159
|
+
raise ArgumentError, "The level '#{level}' does not appear to exist"
|
160
|
+
end
|
161
|
+
if denominator_index.nil?
|
162
|
+
raise ArgumentError, "The denominator level '#{denominator_level}' does not appear to exist"
|
163
|
+
end
|
164
|
+
if hierarchy_levels[hierarchy_name].index(denominator_level) < hierarchy_levels[hierarchy_name].index(level)
|
165
|
+
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}'"
|
166
|
+
end
|
167
|
+
|
168
|
+
q = "select #{level} as level, count(distinct(#{denominator_level})) as level_count from #{table_name} group by #{level}"
|
169
|
+
else
|
170
|
+
q = "select #{level} as level, count(id) as level_count from #{table_name} group by #{level}"
|
171
|
+
end
|
172
|
+
denominators = {}
|
173
|
+
connection.select_all(q).each do |row|
|
174
|
+
denominators[row['level']] = row['level_count'].to_i
|
175
|
+
end
|
176
|
+
denominators
|
177
|
+
end
|
178
|
+
|
179
|
+
# Get the foreign key for this dimension which is used in Fact tables.
|
180
|
+
#
|
181
|
+
# Example: DateDimension would have a foreign key of date_id
|
182
|
+
#
|
183
|
+
# The actual foreign key may be different and depends on the fact class.
|
184
|
+
# You may specify the foreign key to use for a specific fact using the
|
185
|
+
# Fact#set_dimension_options method.
|
186
|
+
def foreign_key
|
187
|
+
table_name.sub(/_dimension/,'') + '_id'
|
188
|
+
end
|
189
|
+
|
190
|
+
# Get an array of the available values for a particular hierarchy level
|
191
|
+
# For example, given a DateDimension with data from 2002 to 2004:
|
192
|
+
#
|
193
|
+
# available_values('calendar_year') returns ['2002','2003','2004']
|
194
|
+
def available_values(level)
|
195
|
+
level_method = level.to_sym
|
196
|
+
level = connection.quote_column_name(level.to_s)
|
197
|
+
order = level_orders[level] || self.order || level
|
198
|
+
|
199
|
+
options = {:select => "distinct #{order.to_s == level.to_s ? '' : order.to_s+','} #{level}", :order => order}
|
200
|
+
values = []
|
201
|
+
find(:all, options).each do |dim|
|
202
|
+
value = dim.send(level_method)
|
203
|
+
values << dim.send(level_method) unless values.include?(value)
|
204
|
+
end
|
205
|
+
values.to_a
|
206
|
+
end
|
207
|
+
|
208
|
+
# Get an array of child values for a particular parent in the hierachy
|
209
|
+
# For example, given a DateDimension with data from 2002 to 2004:
|
210
|
+
#
|
211
|
+
# available_child_values(:cy, [2002, 'Q1']) returns
|
212
|
+
# ['January', 'Feburary', 'March', 'April']
|
213
|
+
def available_child_values(hierarchy_name, parent_values)
|
214
|
+
if hierarchy_levels[hierarchy_name].nil?
|
215
|
+
raise ArgumentError, "The hierarchy '#{hierarchy_name}' does not exist in your dimension #{name}"
|
216
|
+
end
|
217
|
+
|
218
|
+
levels = hierarchy_levels[hierarchy_name]
|
219
|
+
if levels.length <= parent_values.length
|
220
|
+
raise ArgumentError, "The parent_values '#{parent_values.to_yaml}' exceeds the hierarchy depth #{levels.to_yaml}"
|
221
|
+
end
|
222
|
+
|
223
|
+
child_level = levels[parent_values.length].to_s
|
224
|
+
|
225
|
+
# Create the conditions array. Will work with 1.1.6.
|
226
|
+
conditions_parts = []
|
227
|
+
conditions_values = []
|
228
|
+
parent_values.each_with_index do |value, index|
|
229
|
+
conditions_parts << "#{levels[index]} = ?"
|
230
|
+
conditions_values << value
|
231
|
+
end
|
232
|
+
conditions = [conditions_parts.join(' AND ')] + conditions_values unless conditions_parts.empty?
|
233
|
+
|
234
|
+
child_level_method = child_level.to_sym
|
235
|
+
child_level = connection.quote_column_name(child_level)
|
236
|
+
order = level_orders[child_level] || self.order || child_level
|
237
|
+
|
238
|
+
select_sql = "distinct #{child_level}"
|
239
|
+
select_sql += ", #{order}" unless order == child_level
|
240
|
+
options = {:select => select_sql, :order => order}
|
241
|
+
|
242
|
+
options[:conditions] = conditions unless conditions.nil?
|
243
|
+
|
244
|
+
find(:all, options).map do |dim|
|
245
|
+
dim.send(child_level_method)
|
246
|
+
end.uniq
|
247
|
+
end
|
248
|
+
alias :available_children_values :available_child_values
|
249
|
+
|
250
|
+
# Get a tree of Node objects for all of the values in the specified hierarchy.
|
251
|
+
def available_values_tree(hierarchy_name)
|
252
|
+
root = value_tree_cache[hierarchy_name]
|
253
|
+
if root.nil?
|
254
|
+
root = Node.new('All', '__ROOT__')
|
255
|
+
levels = hierarchy(hierarchy_name)
|
256
|
+
nodes = {nil => root}
|
257
|
+
level_list = levels.collect{|level| connection.quote_column_name(level) }.join(',')
|
258
|
+
order = self.order || level_list
|
259
|
+
find(:all, :select => level_list, :group => level_list, :order => order).each do |dim|
|
260
|
+
parent_node = root
|
261
|
+
levels.each do |level|
|
262
|
+
node_value = dim.send(level)
|
263
|
+
child_node = parent_node.optionally_add_child(node_value, level)
|
264
|
+
parent_node = child_node
|
265
|
+
end
|
266
|
+
end
|
267
|
+
value_tree_cache[hierarchy_name] = root
|
268
|
+
end
|
269
|
+
root
|
270
|
+
end
|
271
|
+
|
272
|
+
protected
|
273
|
+
# Get the value tree cache
|
274
|
+
def value_tree_cache
|
275
|
+
@value_tree_cache ||= {}
|
276
|
+
end
|
277
|
+
|
278
|
+
class Node#:nodoc:
|
279
|
+
attr_reader :value, :children, :parent, :level
|
280
|
+
|
281
|
+
def initialize(value, level, parent = nil)
|
282
|
+
@children = []
|
283
|
+
@value = value
|
284
|
+
@parent = parent
|
285
|
+
@level = level
|
286
|
+
end
|
287
|
+
|
288
|
+
def has_child?(child_value)
|
289
|
+
!self.child(child_value).nil?
|
290
|
+
end
|
291
|
+
|
292
|
+
def child(child_value)
|
293
|
+
@children.each do |c|
|
294
|
+
return c if c.value == child_value
|
295
|
+
end
|
296
|
+
nil
|
297
|
+
end
|
298
|
+
|
299
|
+
def add_child(child_value, level)
|
300
|
+
child = Node.new(child_value, level, self)
|
301
|
+
@children << child
|
302
|
+
child
|
303
|
+
end
|
304
|
+
|
305
|
+
def optionally_add_child(child_value, level)
|
306
|
+
c = child(child_value)
|
307
|
+
c = add_child(child_value, level) unless c
|
308
|
+
c
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
public
|
313
|
+
# Expire the value tree cache. This should be called if the dimension
|
314
|
+
def expire_value_tree_cache
|
315
|
+
@value_tree_cache = nil
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
public
|
320
|
+
# Expire the value tree cache
|
321
|
+
def expire_value_tree_cache
|
322
|
+
self.class.expire_value_tree_cache
|
323
|
+
end
|
324
|
+
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
require 'active_warehouse/dimension/date_dimension'
|
329
|
+
require 'active_warehouse/dimension/dimension_view'
|