activewarehouse 0.1.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 +41 -0
- data/Rakefile +121 -0
- data/TODO +4 -0
- data/db/migrations/001_create_table_reports.rb +28 -0
- data/doc/agg_queries.txt +26 -0
- data/doc/agg_queries_results.txt +150 -0
- data/doc/queries.txt +35 -0
- data/generators/cube/USAGE +1 -0
- data/generators/cube/cube_generator.rb +28 -0
- data/generators/cube/templates/model.rb +3 -0
- data/generators/cube/templates/unit_test.rb +8 -0
- data/generators/dimension/USAGE +1 -0
- data/generators/dimension/dimension_generator.rb +46 -0
- data/generators/dimension/templates/fixture.yml +5 -0
- data/generators/dimension/templates/migration.rb +20 -0
- data/generators/dimension/templates/model.rb +3 -0
- data/generators/dimension/templates/unit_test.rb +10 -0
- data/generators/fact/USAGE +1 -0
- data/generators/fact/fact_generator.rb +46 -0
- data/generators/fact/templates/fixture.yml +5 -0
- data/generators/fact/templates/migration.rb +11 -0
- data/generators/fact/templates/model.rb +3 -0
- data/generators/fact/templates/unit_test.rb +10 -0
- data/install.rb +5 -0
- data/lib/active_warehouse.rb +65 -0
- data/lib/active_warehouse/builder.rb +2 -0
- data/lib/active_warehouse/builder/date_dimension_builder.rb +65 -0
- data/lib/active_warehouse/builder/random_data_builder.rb +13 -0
- data/lib/active_warehouse/core_ext.rb +1 -0
- data/lib/active_warehouse/core_ext/time.rb +5 -0
- data/lib/active_warehouse/core_ext/time/calculations.rb +40 -0
- data/lib/active_warehouse/migrations.rb +65 -0
- data/lib/active_warehouse/model.rb +5 -0
- data/lib/active_warehouse/model/aggregate.rb +244 -0
- data/lib/active_warehouse/model/cube.rb +273 -0
- data/lib/active_warehouse/model/dimension.rb +3 -0
- data/lib/active_warehouse/model/dimension/bridge.rb +32 -0
- data/lib/active_warehouse/model/dimension/dimension.rb +152 -0
- data/lib/active_warehouse/model/dimension/hierarchical_dimension.rb +35 -0
- data/lib/active_warehouse/model/fact.rb +96 -0
- data/lib/active_warehouse/model/report.rb +3 -0
- data/lib/active_warehouse/model/report/abstract_report.rb +121 -0
- data/lib/active_warehouse/model/report/chart_report.rb +9 -0
- data/lib/active_warehouse/model/report/table_report.rb +23 -0
- data/lib/active_warehouse/version.rb +9 -0
- data/lib/active_warehouse/view.rb +2 -0
- data/lib/active_warehouse/view/report_helper.rb +213 -0
- data/tasks/active_warehouse_tasks.rake +50 -0
- metadata +144 -0
@@ -0,0 +1,244 @@
|
|
1
|
+
module ActiveWarehouse
|
2
|
+
# An aggreate within a cube used to store calculated values
|
3
|
+
# Each aggregate will contain values for a dimension pair, down each of the dimension hierarchies
|
4
|
+
class Aggregate < ActiveRecord::Base
|
5
|
+
class << self
|
6
|
+
attr_accessor :name, :cube, :dimension1, :dimension2, :dimension1_hierarchy_name, :dimension2_hierarchy_name
|
7
|
+
|
8
|
+
# Get the table name for the aggregate
|
9
|
+
def table_name
|
10
|
+
name = self.name.demodulize.underscore
|
11
|
+
set_table_name(name)
|
12
|
+
name
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the aggregate ID
|
16
|
+
def aggregate_id
|
17
|
+
table_name =~ /(\d+)$/
|
18
|
+
$1.to_i
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns the AggregateMetaData instance associated with this aggregate
|
22
|
+
def meta_data
|
23
|
+
AggregateMetaData.find(aggregate_id)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return true if the aggregate needs to be rebuilt
|
27
|
+
def needs_rebuild?(last_build=nil)
|
28
|
+
return true if meta_data.populated_at.nil?
|
29
|
+
return true if last_build && (meta_data.populated_at < last_build)
|
30
|
+
return false
|
31
|
+
end
|
32
|
+
|
33
|
+
# Return a key for the aggregate
|
34
|
+
def key(dimension1, dimension1_hierarchy, dimension2, dimension2_hierarchy)
|
35
|
+
AggregateKey.new(dimension1, dimension1_hierarchy, dimension2, dimension2_hierarchy)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Create the aggregate table if required. Set force option to true to force creation of the table
|
39
|
+
# if it already exists
|
40
|
+
def create_storage_table(force=false)
|
41
|
+
connection.drop_table(table_name) if force and table_exists?
|
42
|
+
if !table_exists?
|
43
|
+
connection.create_table(table_name, :id => false) do |t|
|
44
|
+
t.column :dimension1_path, :string
|
45
|
+
t.column :dimension1_stage, :integer
|
46
|
+
t.column :dimension2_path, :string
|
47
|
+
t.column :dimension2_stage, :integer
|
48
|
+
cube.fact_class.aggregate_fields.each do |field|
|
49
|
+
#options = cube.fact_class.aggregate_field_options[field]
|
50
|
+
col = cube.fact_class.columns_hash[field.to_s]
|
51
|
+
t.column field, col.type if col
|
52
|
+
end
|
53
|
+
end
|
54
|
+
connection.add_index(table_name, :dimension1_path)
|
55
|
+
connection.add_index(table_name, :dimension1_stage)
|
56
|
+
connection.add_index(table_name, :dimension2_path)
|
57
|
+
connection.add_index(table_name, :dimension2_stage)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Populate the aggregate table
|
62
|
+
def populate
|
63
|
+
# create the storage table if necessary
|
64
|
+
create_storage_table
|
65
|
+
|
66
|
+
#puts "Populating aggregate table #{table_name}"
|
67
|
+
# clear out the current data
|
68
|
+
#connection.execute("TRUNCATE TABLE #{table_name}") #TODO: make this generic to support all databases
|
69
|
+
delete_all
|
70
|
+
|
71
|
+
# aggregate the data for the two dimensions
|
72
|
+
fact_class = cube.fact_class
|
73
|
+
dim1 = Dimension.class_name(dimension1).constantize
|
74
|
+
dim2 = Dimension.class_name(dimension2).constantize
|
75
|
+
dim1_stage_path = []
|
76
|
+
dim1.hierarchy(meta_data.dimension1_hierarchy.to_sym).each_with_index do |dim1_stage_name, dim1_stage_level|
|
77
|
+
dim1_stage_path << dim1_stage_name
|
78
|
+
dim2_stage_path = []
|
79
|
+
dim2.hierarchy(meta_data.dimension2_hierarchy.to_sym).each_with_index do |dim2_stage_name, dim2_stage_level|
|
80
|
+
dim2_stage_path << dim2_stage_name
|
81
|
+
|
82
|
+
stmt, fields = build_query(fact_class, dim1, dim1_stage_path, dim2, dim2_stage_path)
|
83
|
+
|
84
|
+
# Get the facts and aggregate them
|
85
|
+
fact_class.connection.execute(stmt).each do |row|
|
86
|
+
dim1_value = []
|
87
|
+
dim1_stage_path.each_with_index do |v, index|
|
88
|
+
dim1_value << row[index]
|
89
|
+
end
|
90
|
+
dim2_value = []
|
91
|
+
dim2_stage_path.each_with_index do |v, index|
|
92
|
+
dim2_value << row[dim1_stage_path.length + index]
|
93
|
+
end
|
94
|
+
|
95
|
+
agg_instance = new
|
96
|
+
agg_instance.dimension1_path = dim1_value.join(':')
|
97
|
+
agg_instance.dimension1_stage = dim1_stage_level
|
98
|
+
agg_instance.dimension2_path = dim2_value.join(':')
|
99
|
+
agg_instance.dimension2_stage = dim2_stage_level
|
100
|
+
fields.each_with_index do |field, index|
|
101
|
+
# do the average here
|
102
|
+
agg_instance.send("#{field}=".to_sym, row[index + dim1_value.length + dim2_value.length])
|
103
|
+
end
|
104
|
+
agg_instance.save!
|
105
|
+
|
106
|
+
meta_data.update_attribute(:populated_at, Time.now)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Build the aggregation query for the given dimensions and stage paths
|
113
|
+
def build_query(fact_class, dim1, dim1_stage_path, dim2, dim2_stage_path)
|
114
|
+
dim1_group = dim1_stage_path.collect { |p| "d1.#{p}"}.join(", ")
|
115
|
+
dim2_group = dim2_stage_path.collect { |p| "d2.#{p}"}.join(", ")
|
116
|
+
|
117
|
+
# Set up the find options
|
118
|
+
fact_find_options = {}
|
119
|
+
fact_find_options[:group] = "#{dim1_group}, #{dim2_group}"
|
120
|
+
fact_find_options[:joins] = "join #{dim1.table_name} d1 on f.#{dim1.foreign_key} = d1.id"
|
121
|
+
fact_find_options[:joins] << " join #{dim2.table_name} d2 on f.#{dim2.foreign_key} = d2.id"
|
122
|
+
|
123
|
+
# Build the 'select' part of the query
|
124
|
+
# denominator = nil
|
125
|
+
fields = []
|
126
|
+
fact_select = ["#{dim1_group}, #{dim2_group}"]
|
127
|
+
fact_class.aggregate_fields.each do |field_name|
|
128
|
+
options = fact_class.aggregate_field_options[field_name]
|
129
|
+
fields << field_name
|
130
|
+
|
131
|
+
options[:type] ||= :sum
|
132
|
+
case options[:type]
|
133
|
+
when :sum
|
134
|
+
fact_select << " sum(f.#{field_name}) as #{field_name}"
|
135
|
+
when Hash
|
136
|
+
if options[:type][dim1.sym] == :average && options[:type][dim2.sym] == :average
|
137
|
+
# I believe this is a special case, but I'm not sure how yet. If both dimensions are defined
|
138
|
+
# averages then perhaps that value cannot be calculated at all. TODO: research
|
139
|
+
else
|
140
|
+
fact_select << " sum(f.#{field_name}) as #{field_name}"
|
141
|
+
end
|
142
|
+
else
|
143
|
+
raise "Unsupported aggregate type: #{options[:type]}"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
fact_find_options[:select] = fact_select.join(',')
|
147
|
+
|
148
|
+
# put the SQL statement together
|
149
|
+
stmt = "select #{fact_find_options[:select]} from "
|
150
|
+
stmt << "#{fact_class.table_name} f #{fact_find_options[:joins]} "
|
151
|
+
stmt << "group by #{fact_find_options[:group]}"
|
152
|
+
|
153
|
+
return stmt, fields
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
|
158
|
+
public
|
159
|
+
# Clone and reset at the same time
|
160
|
+
def clone_and_reset
|
161
|
+
o = clone
|
162
|
+
o.reset
|
163
|
+
o
|
164
|
+
end
|
165
|
+
|
166
|
+
def non_data_fields
|
167
|
+
['dimension1_path','dimension1_stage','dimension2_path','dimension2_stage']
|
168
|
+
end
|
169
|
+
|
170
|
+
def data_fields
|
171
|
+
fields = []
|
172
|
+
self.class.columns.each do |column|
|
173
|
+
unless non_data_fields.include?(column.name)
|
174
|
+
fields << column.name
|
175
|
+
end
|
176
|
+
end
|
177
|
+
fields
|
178
|
+
end
|
179
|
+
|
180
|
+
# Reset the aggregate
|
181
|
+
def reset
|
182
|
+
self.class.columns.each do |column|
|
183
|
+
unless non_data_fields.include?(column.name)
|
184
|
+
value = column.number? ? 0 : 'None'
|
185
|
+
send("#{column.name}=".to_sym, value)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# ActiveRecord object which stores meta data about the aggregate
|
192
|
+
class AggregateMetaData < ActiveRecord::Base
|
193
|
+
# Build the underlying table. Set force to true to force the build of the table
|
194
|
+
def self.build_table(force=false)
|
195
|
+
connection.drop_table(table_name) if force and table_exists?
|
196
|
+
if !table_exists?
|
197
|
+
connection.create_table(table_name) do |t|
|
198
|
+
t.column :cube_name, :string
|
199
|
+
t.column :dimension1, :string
|
200
|
+
t.column :dimension1_hierarchy, :string
|
201
|
+
t.column :dimension2, :string
|
202
|
+
t.column :dimension2_hierarchy, :string
|
203
|
+
t.column :created_at, :datetime
|
204
|
+
t.column :populated_at, :datetime
|
205
|
+
end
|
206
|
+
connection.add_index table_name, :cube_name
|
207
|
+
end
|
208
|
+
end
|
209
|
+
def key
|
210
|
+
Aggregate.key(dimension1, dimension1_hierarchy, dimension2, dimension2_hierarchy)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Key for aggregate caching
|
215
|
+
class AggregateKey
|
216
|
+
attr_reader :dimension1, :dimension1_hierarchy, :dimension2, :dimension2_hierarchy
|
217
|
+
|
218
|
+
def initialize(dimension1, dimension1_hierarchy, dimension2, dimension2_hierarchy)
|
219
|
+
@dimension1 = dimension1
|
220
|
+
@dimension1_hierarchy = dimension1_hierarchy
|
221
|
+
@dimension2 = dimension2
|
222
|
+
@dimension2_hierarchy = dimension2_hierarchy
|
223
|
+
end
|
224
|
+
|
225
|
+
def ==(o)
|
226
|
+
o.instance_of?(self.class) and (o.to_s == to_s or o.to_s = to_rs)
|
227
|
+
end
|
228
|
+
|
229
|
+
def hash
|
230
|
+
to_s.hash
|
231
|
+
end
|
232
|
+
|
233
|
+
def to_s
|
234
|
+
"#{@dimension1}.#{@dimension1_hierarchy}.#{@dimension2}.#{@dimension2_hierarchy}"
|
235
|
+
end
|
236
|
+
|
237
|
+
# Return the "reveresed" version of this key String representation
|
238
|
+
def to_rs
|
239
|
+
"#{@dimension2}.#{@dimension2_hierarchy}.#{@dimension1}.#{@dimension1_hierarchy}"
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
ActiveWarehouse::AggregateMetaData.build_table
|
@@ -0,0 +1,273 @@
|
|
1
|
+
module ActiveWarehouse
|
2
|
+
# A Cube represents a collection of dimensions operating on a fact. The Cube provides a front-end for getting at the
|
3
|
+
# underlying data. The Cube manages the creation and population of all underlying aggregates.
|
4
|
+
class Cube
|
5
|
+
class << self
|
6
|
+
# Callback which is invoked when subclasses are created
|
7
|
+
def inherited(subclass)
|
8
|
+
subclasses << subclass
|
9
|
+
end
|
10
|
+
|
11
|
+
# Get a list of all known subclasses
|
12
|
+
def subclasses
|
13
|
+
@subclasses ||= []
|
14
|
+
end
|
15
|
+
|
16
|
+
# Defines the dimensions that this cube pivots on.
|
17
|
+
def pivots_on(*dimension_list)
|
18
|
+
# TODO: Validate if the fact is set
|
19
|
+
dimension_list.each do |dimension|
|
20
|
+
dimensions << dimension
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Defines the fact that this cube reports on
|
25
|
+
def reports_on(fact)
|
26
|
+
# TODO: Validate if one or more dimension is set
|
27
|
+
@fact = fact
|
28
|
+
end
|
29
|
+
|
30
|
+
# Rebuild all aggregate classes. Set :force => true to force the rebuild of aggregate classes.
|
31
|
+
def rebuild(options={})
|
32
|
+
logger.debug "Rebuilding aggregates for cube #{name}"
|
33
|
+
options[:force] ||= false
|
34
|
+
build_aggregate_classes(options)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Populate all aggregates. Set :force => true to force the population of the aggregate class.
|
38
|
+
def populate(options={})
|
39
|
+
options[:force] ||= false
|
40
|
+
aggregates.each do |agg_id, agg_clazz|
|
41
|
+
if agg_clazz.needs_rebuild? || options[:force]
|
42
|
+
logger.debug "Populating aggregate class #{agg_clazz.name}"
|
43
|
+
agg_clazz.populate
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Get the fact that this cube reports on
|
49
|
+
def fact
|
50
|
+
@fact
|
51
|
+
end
|
52
|
+
|
53
|
+
# Get the dimensions that this cube pivots on
|
54
|
+
def dimensions
|
55
|
+
@dimensions ||= []
|
56
|
+
end
|
57
|
+
|
58
|
+
# Get the aggregate classes for this dimension
|
59
|
+
def aggregates
|
60
|
+
rebuild if @aggregates.nil?
|
61
|
+
@aggregates
|
62
|
+
end
|
63
|
+
|
64
|
+
# Get the class name for the specified cube name
|
65
|
+
# Example: Regional Sales will become RegionalSalesCube
|
66
|
+
def class_name(name)
|
67
|
+
cube_name = name.to_s
|
68
|
+
cube_name = "#{cube_name}_cube" unless cube_name =~ /_cube$/
|
69
|
+
cube_name.classify
|
70
|
+
end
|
71
|
+
|
72
|
+
# Get the aggregated fact class name
|
73
|
+
def fact_class_name
|
74
|
+
Fact.class_name(fact)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Get the aggregated fact class instance
|
78
|
+
def fact_class
|
79
|
+
fact_class_name.constantize
|
80
|
+
end
|
81
|
+
|
82
|
+
# Get a list of dimension class instances
|
83
|
+
def dimension_classes
|
84
|
+
dimensions.collect {|dimension| Dimension.class_name(dimension).constantize}
|
85
|
+
end
|
86
|
+
|
87
|
+
def logger
|
88
|
+
@logger ||= Logger.new('cube.log')
|
89
|
+
end
|
90
|
+
|
91
|
+
def last_modified
|
92
|
+
lm = Fact.class_for_name(fact).last_modified
|
93
|
+
dimensions.each do |dimension|
|
94
|
+
dim = Dimension.class_for_name(dimension)
|
95
|
+
lm = dim.last_modified if dim.last_modified > lm
|
96
|
+
end
|
97
|
+
lm
|
98
|
+
end
|
99
|
+
|
100
|
+
protected
|
101
|
+
def build_aggregate_classes(options={})
|
102
|
+
@aggregates = {}
|
103
|
+
existing_dimension_pairs = []
|
104
|
+
logger.debug "Building aggregate classes"
|
105
|
+
dimensions.each do |column_dimension|
|
106
|
+
dimensions.each do |row_dimension|
|
107
|
+
next if column_dimension == row_dimension
|
108
|
+
next if existing_dimension_pairs.include? [column_dimension,row_dimension]
|
109
|
+
next if existing_dimension_pairs.include? [row_dimension,column_dimension]
|
110
|
+
|
111
|
+
existing_dimension_pairs << [column_dimension,row_dimension]
|
112
|
+
col_dim_class = Dimension.class_for_name(column_dimension)
|
113
|
+
col_dim_class.hierarchy_levels.each_key do |column_hierarchy_name|
|
114
|
+
row_dim_class = Dimension.class_for_name(row_dimension)
|
115
|
+
row_dim_class.hierarchy_levels.each_key do |row_hierarchy_name|
|
116
|
+
# Construct the aggregate meta data instance
|
117
|
+
meta_data_attributes = {
|
118
|
+
:cube_name => self.name,
|
119
|
+
:dimension1 => column_dimension.to_s,
|
120
|
+
:dimension1_hierarchy => column_hierarchy_name.to_s,
|
121
|
+
:dimension2 => row_dimension.to_s,
|
122
|
+
:dimension2_hierarchy => row_hierarchy_name.to_s
|
123
|
+
}
|
124
|
+
conditions = []
|
125
|
+
condition_args = []
|
126
|
+
meta_data_attributes.each do |key, value|
|
127
|
+
conditions << "#{key} = ?"
|
128
|
+
condition_args << value
|
129
|
+
end
|
130
|
+
conditions = [conditions.join(' and ')] + condition_args
|
131
|
+
meta_data = AggregateMetaData.find(:first, :conditions => conditions)
|
132
|
+
unless meta_data
|
133
|
+
meta_data = AggregateMetaData.create(meta_data_attributes)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Construct the aggregate class instance
|
137
|
+
aggregate_class = Class.new(ActiveWarehouse::Aggregate)
|
138
|
+
aggregate_class.name = "Agg#{meta_data.id}"
|
139
|
+
logger.debug "Constructed aggregate #{aggregate_class.name}"
|
140
|
+
aggregate_class.cube = self
|
141
|
+
aggregate_class.dimension1 = column_dimension
|
142
|
+
aggregate_class.dimension1_hierarchy_name = column_hierarchy_name
|
143
|
+
aggregate_class.dimension2 = row_dimension
|
144
|
+
aggregate_class.dimension2_hierarchy_name = row_hierarchy_name
|
145
|
+
|
146
|
+
# Create the underlying aggregate storage table
|
147
|
+
# TODO: fix the bug of data not being found when a storage table rebuild occurs
|
148
|
+
force_storage_table_rebuild = options[:force] || aggregate_class.needs_rebuild?(last_modified)
|
149
|
+
logger.debug "Force storage table rebuild? #{force_storage_table_rebuild}"
|
150
|
+
aggregate_class.create_storage_table(force_storage_table_rebuild)
|
151
|
+
|
152
|
+
# Keep a reference to the aggregate class instance
|
153
|
+
@aggregates[meta_data.id] = aggregate_class
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
public
|
163
|
+
def aggregate_map(column_dimension, column_hierarchy, row_dimension, row_hierarchy, cstage=0, rstage=0)
|
164
|
+
# Fill known cells
|
165
|
+
agg_map = AggregateMap.new
|
166
|
+
agg_records = aggregate_records(column_dimension, column_hierarchy, row_dimension, row_hierarchy)
|
167
|
+
agg_records.each do |agg_record|
|
168
|
+
# agg_record is an instance of Aggregate
|
169
|
+
# collect the aggregate record data fields into an array
|
170
|
+
data_array = agg_record.data_fields.collect{ |data_field_name| agg_record.send(data_field_name.to_sym) }
|
171
|
+
|
172
|
+
# convert to an average where necessary
|
173
|
+
# TODO: implement
|
174
|
+
|
175
|
+
# add calculated fields to the data array
|
176
|
+
self.class.fact_class.calculated_fields.each do |calculated_field|
|
177
|
+
options = self.class.fact_class.calculated_field_options[calculated_field]
|
178
|
+
data_array << options[:block].call(agg_record)
|
179
|
+
end
|
180
|
+
|
181
|
+
# add the data array to the aggregate map
|
182
|
+
agg_map.add_data(agg_record.dimension2_path, agg_record.dimension1_path, data_array)
|
183
|
+
end
|
184
|
+
agg_map
|
185
|
+
end
|
186
|
+
|
187
|
+
protected
|
188
|
+
# Return all of the Aggregate records for the specified dimensions and hierarchies
|
189
|
+
def aggregate_records(column_dimension, column_hierarchy, row_dimension, row_hierarchy)
|
190
|
+
k = Aggregate.key(column_dimension, column_hierarchy, row_dimension, row_hierarchy)
|
191
|
+
if aggregates[k].nil?
|
192
|
+
self.class.logger.debug("Aggregate #{k} not found in cache")
|
193
|
+
conditions = ['cube_name = ?', self.class.name]
|
194
|
+
conditions[0] << ' and dimension1 = ? and dimension1_hierarchy = ? and dimension2 = ? and dimension2_hierarchy = ?'
|
195
|
+
conditions << column_dimension.to_s
|
196
|
+
conditions << column_hierarchy.to_s
|
197
|
+
conditions << row_dimension.to_s
|
198
|
+
conditions << row_hierarchy.to_s
|
199
|
+
|
200
|
+
conditions_reversed = ['cube_name = ?', self.class.name]
|
201
|
+
conditions_reversed[0] << ' and dimension1 = ? and dimension1_hierarchy = ? and dimension2 = ? and dimension2_hierarchy = ?'
|
202
|
+
conditions_reversed << row_dimension.to_s
|
203
|
+
conditions_reversed << row_hierarchy.to_s
|
204
|
+
conditions_reversed << column_dimension.to_s
|
205
|
+
conditions_reversed << column_hierarchy.to_s
|
206
|
+
|
207
|
+
aggregate_meta_data = AggregateMetaData.find(:first, :conditions => conditions)
|
208
|
+
aggregate_meta_data ||= AggregateMetaData.find(:first, :conditions => conditions_reversed)
|
209
|
+
if aggregate_meta_data.nil?
|
210
|
+
self.class.rebuild
|
211
|
+
aggregate_meta_data = AggregateMetaData.find(:first, :conditions => conditions)
|
212
|
+
raise "Cannot find aggregate meta data for key #{k}" if aggregate_meta_data.nil?
|
213
|
+
end
|
214
|
+
aggregate_class = self.class.aggregates[aggregate_meta_data.id]
|
215
|
+
if aggregate_class.nil?
|
216
|
+
self.class.rebuild
|
217
|
+
aggregate_class = self.class.aggregates[aggregate_meta_data.id]
|
218
|
+
raise "Cannot find aggregate for id #{aggregate_meta_data.id}" if aggregate_class.nil?
|
219
|
+
end
|
220
|
+
#puts "Loading aggregate #{aggregate_meta_data.id}"
|
221
|
+
aggregates[k] = aggregate_class.find(:all)
|
222
|
+
end
|
223
|
+
aggregates[k]
|
224
|
+
end
|
225
|
+
|
226
|
+
# Get a hash of all aggregate data
|
227
|
+
def aggregates
|
228
|
+
@aggregates ||= {}
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# In-memory map of aggregate values
|
233
|
+
class AggregateMap
|
234
|
+
attr_reader :length
|
235
|
+
|
236
|
+
# Initialize the aggregate map
|
237
|
+
def initialize
|
238
|
+
@m = {}
|
239
|
+
end
|
240
|
+
|
241
|
+
# Return true if the aggregate map includes the specified row path
|
242
|
+
def has_row_path?(row_path)
|
243
|
+
@m.has_key?(row_path)
|
244
|
+
end
|
245
|
+
|
246
|
+
# Get the value for the specified row path, column path and field index
|
247
|
+
def value(row_path, col_path, field_index)
|
248
|
+
#puts "Getting value for #{row_path}, #{col_path} [field=#{field_index}]"
|
249
|
+
row = @m[row_path]
|
250
|
+
return 0 if row.nil?
|
251
|
+
col = row[col_path]
|
252
|
+
return 0 if col.nil?
|
253
|
+
return col[field_index] || 0
|
254
|
+
end
|
255
|
+
|
256
|
+
# Get an array of the values for the specified row path and column path
|
257
|
+
def values(row_path, col_path)
|
258
|
+
row = @m[row_path]
|
259
|
+
return Array.new(length, 0) if row.nil?
|
260
|
+
col = row[col_path]
|
261
|
+
return Array.new(length, 0) if col.nil?
|
262
|
+
col
|
263
|
+
end
|
264
|
+
|
265
|
+
# Add an array of data for the given row and column path
|
266
|
+
def add_data(row_path, col_path, data_array)
|
267
|
+
@length ||= data_array.length
|
268
|
+
#puts "Adding data for #{row_path}, #{col_path} [data=[#{data_array.join(',')}]]"
|
269
|
+
@m[row_path] ||= {}
|
270
|
+
@m[row_path][col_path] = data_array
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|