activewarehouse 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,32 @@
|
|
1
|
+
module ActiveWarehouse
|
2
|
+
class Bridge < ActiveRecord::Base
|
3
|
+
class << self
|
4
|
+
# Get the table name. By default the table name will be the name of the dimension in singular form.
|
5
|
+
#
|
6
|
+
# Example: DateDimension will have a table called date_dimension
|
7
|
+
def table_name
|
8
|
+
name = self.name.demodulize.underscore
|
9
|
+
set_table_name(name)
|
10
|
+
name
|
11
|
+
end
|
12
|
+
|
13
|
+
def build_table(force=false)
|
14
|
+
connection.drop_table(table_name) if force and table_exists?
|
15
|
+
if !table_exists?
|
16
|
+
connection.create_table(table_name, :id => false) do |t|
|
17
|
+
t.column :parent_id, :integer
|
18
|
+
t.column :child_id, :integer
|
19
|
+
t.column :levels_from_parent, :integer
|
20
|
+
t.column :bottom_flag, :boolean
|
21
|
+
t.column :top_flag, :boolean
|
22
|
+
end
|
23
|
+
connection.add_index table_name, :parent_id
|
24
|
+
connection.add_index table_name, :child_id
|
25
|
+
connection.add_index table_name, :levels_from_parent
|
26
|
+
connection.add_index table_name, :bottom_flag
|
27
|
+
connection.add_index table_name, :top_flag
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
module ActiveWarehouse
|
2
|
+
class Dimension < ActiveRecord::Base
|
3
|
+
class << self
|
4
|
+
|
5
|
+
# Indicate that this dimension is a variable depth hierarchical dimension. Calling this method will
|
6
|
+
# mix-in the ActiveWarehouse::HierarchicalDimension module, adding methods to support the hierarchical
|
7
|
+
# representation of this dimension
|
8
|
+
def acts_as_hierarchy_dimension
|
9
|
+
# Mix in the hierarchy dimension support code
|
10
|
+
include ActiveWarehouse::HierarchicalDimension
|
11
|
+
end
|
12
|
+
|
13
|
+
# Define a named attribute hierarchy in the dimension.
|
14
|
+
#
|
15
|
+
# Example: define_hierarchy(:fiscal_calendar, [:fiscal_year, :fiscal_quarter, :fiscal_month])
|
16
|
+
#
|
17
|
+
# This would indicate that one of the drill down paths for this dimension is:
|
18
|
+
# Fiscal Year -> Fiscal Quarter -> Fiscal Month
|
19
|
+
#
|
20
|
+
# Internally the hierarchies are stored in order. The first hierarchy defined will be used as the default
|
21
|
+
# if no hierarchy is specified when rendering a cube.
|
22
|
+
def define_hierarchy(name, levels)
|
23
|
+
hierarchies << name
|
24
|
+
hierarchy_levels[name] = levels
|
25
|
+
end
|
26
|
+
|
27
|
+
# Get the named attribute hierarchy
|
28
|
+
#
|
29
|
+
# Example: hierarchy(:fiscal_calendar) might return [:fiscal_year, :fiscal_quarter, :fiscal_month]
|
30
|
+
def hierarchy(name)
|
31
|
+
hierarchy_levels[name]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get the ordered hierarchy names
|
35
|
+
def hierarchies
|
36
|
+
@hierarchies ||= []
|
37
|
+
end
|
38
|
+
|
39
|
+
# Get the hierarchy levels hash
|
40
|
+
def hierarchy_levels
|
41
|
+
@hierarchy_levels ||= {}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Return a symbol used when referring to this dimension. The symbol is calculated by demodulizing and underscoring the
|
45
|
+
# dimension's class name and then removing the trailing _dimension.
|
46
|
+
#
|
47
|
+
# Example: DateDimension will return a symbol :date
|
48
|
+
def sym
|
49
|
+
self.name.demodulize.underscore.gsub(/_dimension/, '').to_sym
|
50
|
+
end
|
51
|
+
|
52
|
+
# Get the table name. By default the table name will be the name of the dimension in singular form.
|
53
|
+
#
|
54
|
+
# Example: DateDimension will have a table called date_dimension
|
55
|
+
def table_name
|
56
|
+
name = self.name.demodulize.underscore
|
57
|
+
set_table_name(name)
|
58
|
+
name
|
59
|
+
end
|
60
|
+
|
61
|
+
# Convert the given name into a dimension class name
|
62
|
+
def class_name(name)
|
63
|
+
dimension_name = name.to_s
|
64
|
+
dimension_name = "#{dimension_name}_dimension" unless dimension_name =~ /_dimension$/
|
65
|
+
dimension_name.classify
|
66
|
+
end
|
67
|
+
|
68
|
+
# Get a class for the specified named dimension
|
69
|
+
def class_for_name(name)
|
70
|
+
class_name(name).constantize
|
71
|
+
end
|
72
|
+
|
73
|
+
# Return the time when the underlying dimension source file was last modified. This is used
|
74
|
+
# to determine if a cube structure rebuild is required
|
75
|
+
def last_modified
|
76
|
+
File.new(__FILE__).mtime
|
77
|
+
end
|
78
|
+
|
79
|
+
# Get the dimension class for the specified dimension parameter. The dimension parameter may be a class,
|
80
|
+
# String or Symbol.
|
81
|
+
def to_dimension(dimension)
|
82
|
+
return dimension if dimension.is_a?(Class) and dimension.superclass == Dimension
|
83
|
+
return class_for_name(dimension)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns a hash of all of the values at the specified hierarchy level mapped to the count at that level.
|
87
|
+
# For example, given a date dimension with years from 2002 to 2004 and a hierarchy defined with:
|
88
|
+
#
|
89
|
+
# hierarchy :cy, [:calendar_year, :calendar_quarter, :calendar_month_name]
|
90
|
+
#
|
91
|
+
# ...then...
|
92
|
+
#
|
93
|
+
# DateDimension.denominator_count(:cy, :calendar_year, :calendar_quarter) returns {'2002' => 4, '2003' => 4, '2004' => 4}
|
94
|
+
#
|
95
|
+
# If the denominator_level parameter is omitted or nil then:
|
96
|
+
#
|
97
|
+
# DateDimension.denominator_count(:cy, :calendar_year) returns {'2003' => 365, '2003' => 365, '2004' => 366}
|
98
|
+
#
|
99
|
+
def denominator_count(hierarchy_name, level, denominator_level=nil)
|
100
|
+
if hierarchy_levels[hierarchy_name].nil?
|
101
|
+
raise ArgumentError, "The hierarchy '#{hierarchy_name}' does not exist in your dimension #{name}"
|
102
|
+
end
|
103
|
+
|
104
|
+
q = nil
|
105
|
+
# If the denominator_level is specified and it is not the last element in the hierarchy then do a distinct count. If
|
106
|
+
# the denominator level is less than the current level then raise an ArgumentError. In other words, if the current level is
|
107
|
+
# calendar month then passing in calendar year as the denominator level would raise an ArgumentErro.
|
108
|
+
#
|
109
|
+
# If the denominator_level is not specified then assume the finest grain possible (in the context of a date dimension
|
110
|
+
# this would be each day) and use the id to count.
|
111
|
+
if denominator_level && hierarchy_levels[hierarchy_name].last != denominator_level
|
112
|
+
level_index = hierarchy_levels[hierarchy_name].index(level)
|
113
|
+
denominator_index = hierarchy_levels[hierarchy_name].index(denominator_level)
|
114
|
+
|
115
|
+
if level_index.nil?
|
116
|
+
raise ArgumentError, "The level '#{level}' does not appear to exist"
|
117
|
+
end
|
118
|
+
if denominator_index.nil?
|
119
|
+
raise ArgumentError, "The denominator level '#{denominator_level}' does not appear to exist"
|
120
|
+
end
|
121
|
+
if hierarchy_levels[hierarchy_name].index(denominator_level) < hierarchy_levels[hierarchy_name].index(level)
|
122
|
+
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}'"
|
123
|
+
end
|
124
|
+
|
125
|
+
q = "select #{level}, count(distinct(#{denominator_level})) from #{table_name} group by #{level}"
|
126
|
+
else
|
127
|
+
q = "select #{level}, count(id) from #{table_name} group by #{level}"
|
128
|
+
end
|
129
|
+
denominators = {}
|
130
|
+
connection.execute(q).each do |row|
|
131
|
+
denominators[row[0]] = row[1].to_i
|
132
|
+
end
|
133
|
+
denominators
|
134
|
+
end
|
135
|
+
|
136
|
+
# Get the foreign key for this dimension which is used in Fact tables.
|
137
|
+
#
|
138
|
+
# Example: DateDimension would have a foreign key of date_id
|
139
|
+
def foreign_key
|
140
|
+
table_name.sub(/_dimension/,'') + '_id'
|
141
|
+
end
|
142
|
+
|
143
|
+
# Get an array of the available values for a particular hierarchy level
|
144
|
+
# For example, given a DateDimension with data from 2002 to 2004:
|
145
|
+
#
|
146
|
+
# available_values('calendar_year') returns ['2002','2003','2004']
|
147
|
+
def available_values(level)
|
148
|
+
find(:all, :group => level.to_s, :order => level.to_s).collect {|dim| dim.send(level.to_sym)}
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module ActiveWarehouse
|
2
|
+
module HierarchicalDimension
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
# Get the bridge class for this hierarchical dimension
|
9
|
+
def bridge_class
|
10
|
+
unless @bridge_class
|
11
|
+
@bridge_class = Class.new(ActiveWarehouse::Bridge)
|
12
|
+
Object.const_set(bridge_class_name, @bridge_class)
|
13
|
+
end
|
14
|
+
@bridge_class
|
15
|
+
end
|
16
|
+
|
17
|
+
# Get the bridge class name for this hierarchical dimension
|
18
|
+
def bridge_class_name
|
19
|
+
name.gsub(/Dimension$/, '') + 'HierarchyBridge'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Get the parent for this node
|
24
|
+
def parent
|
25
|
+
bridge = bridge_class.new
|
26
|
+
bridge.find(:first, :conditions => ['levels_from_parent = 1 and child_id = ?', self.id])
|
27
|
+
end
|
28
|
+
|
29
|
+
# Get the children for this node
|
30
|
+
def children
|
31
|
+
bridge = bridge_class.new
|
32
|
+
bridge.find(:all, :conditions => ['levels_from_parent = 1 and parent_id = ?', self.id])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module ActiveWarehouse
|
2
|
+
class Fact < ActiveRecord::Base
|
3
|
+
|
4
|
+
class << self
|
5
|
+
attr_accessor :aggregate_fields, :aggregate_field_options, :calculated_fields, :calculated_field_options
|
6
|
+
|
7
|
+
# Return a list of dimensions for this fact.
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
#
|
11
|
+
# sales_fact
|
12
|
+
# time_id
|
13
|
+
# region_id
|
14
|
+
# sales_amount
|
15
|
+
# number_items_sold
|
16
|
+
#
|
17
|
+
# Calling SalesFact.dimensions would return the list: ['time','region']
|
18
|
+
def dimensions
|
19
|
+
dims = []
|
20
|
+
columns.each do |column|
|
21
|
+
if column.name =~ /(.*)_id/
|
22
|
+
dims << $1.to_sym
|
23
|
+
end
|
24
|
+
end
|
25
|
+
dims
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get the time when the fact source file was last modified
|
29
|
+
def last_modified
|
30
|
+
File.new(__FILE__).mtime
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get the table name. The fact table name is pluralized
|
34
|
+
def table_name
|
35
|
+
name = self.name.demodulize.underscore.pluralize
|
36
|
+
set_table_name(name)
|
37
|
+
name
|
38
|
+
end
|
39
|
+
|
40
|
+
# Get the class name for the specified fact name
|
41
|
+
def class_name(name)
|
42
|
+
fact_name = name.to_s
|
43
|
+
fact_name = "#{fact_name}_facts" unless fact_name =~ /_fact[s?]$/
|
44
|
+
fact_name.classify
|
45
|
+
end
|
46
|
+
|
47
|
+
# Get the class for the specified fact name
|
48
|
+
def class_for_name(name)
|
49
|
+
class_name(name).constantize
|
50
|
+
end
|
51
|
+
|
52
|
+
# Define an aggregate. Also aliased from aggregate()
|
53
|
+
# * <tt>field</tt>: The field name
|
54
|
+
# * <tt>options</tt>: A hash of options for the aggregate
|
55
|
+
def define_aggregate(field, options={})
|
56
|
+
aggregate_fields << field
|
57
|
+
options[:type] ||= :sum
|
58
|
+
aggregate_field_options[field] = options
|
59
|
+
end
|
60
|
+
alias :aggregate :define_aggregate
|
61
|
+
|
62
|
+
# Define a calculated field
|
63
|
+
# * <tt>field</tt>: The field name
|
64
|
+
# * <tt>options</tt>: An options hash
|
65
|
+
#
|
66
|
+
# This method takes a block which will be passed the current aggregate record.
|
67
|
+
#
|
68
|
+
# Example: calculated_field (:gross_margin) { |r| r.gross_profit_dollar_amount / r.sales_dollar_amount}
|
69
|
+
def calculated_field(field, options={}, &block)
|
70
|
+
calculated_fields << field
|
71
|
+
options[:block] = block
|
72
|
+
calculated_field_options[field] = options
|
73
|
+
end
|
74
|
+
|
75
|
+
# Get a list of all calculated fields
|
76
|
+
def calculated_fields
|
77
|
+
@calculated_field ||= []
|
78
|
+
end
|
79
|
+
|
80
|
+
# Get a hash of all calculated field options
|
81
|
+
def calculated_field_options
|
82
|
+
@calculated_field_options ||= {}
|
83
|
+
end
|
84
|
+
|
85
|
+
# Get a list of all aggregate fields
|
86
|
+
def aggregate_fields
|
87
|
+
@aggregate_fields ||= []
|
88
|
+
end
|
89
|
+
|
90
|
+
# Get a hash of all aggregate field options
|
91
|
+
def aggregate_field_options
|
92
|
+
@aggregate_field_options ||= {}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module ActiveWarehouse
|
2
|
+
module Report
|
3
|
+
module AbstractReport
|
4
|
+
|
5
|
+
attr_accessor :pass_params
|
6
|
+
|
7
|
+
# Set the cube name
|
8
|
+
def cube_name=(name)
|
9
|
+
self['cube_name'] = name
|
10
|
+
@cube = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
# Get the current cube instance
|
14
|
+
def cube
|
15
|
+
if @cube.nil?
|
16
|
+
cube_class = ActiveWarehouse::Cube.class_name(self.cube_name).constantize
|
17
|
+
@cube = cube_class.new
|
18
|
+
end
|
19
|
+
@cube
|
20
|
+
end
|
21
|
+
|
22
|
+
# Get the fact class
|
23
|
+
def fact_class
|
24
|
+
cube.class.fact_class
|
25
|
+
end
|
26
|
+
|
27
|
+
# Get the column dimension class
|
28
|
+
def column_dimension_class
|
29
|
+
@column_dimension_class ||= ActiveWarehouse::Dimension.class_name(self.column_dimension_name).constantize
|
30
|
+
end
|
31
|
+
|
32
|
+
def column_hierarchy
|
33
|
+
self['column_hierarchy'] || column_dimension_class.hierarchies.first
|
34
|
+
end
|
35
|
+
|
36
|
+
def column_param_prefix
|
37
|
+
self['column_param_prefix'] || 'c'
|
38
|
+
end
|
39
|
+
|
40
|
+
# Get the row dimension class
|
41
|
+
def row_dimension_class
|
42
|
+
@row_dimension_class ||= ActiveWarehouse::Dimension.class_name(self.row_dimension_name).constantize
|
43
|
+
end
|
44
|
+
|
45
|
+
def row_hierarchy
|
46
|
+
self['row_hierarchy'] || row_dimension_class.hierarchies.first
|
47
|
+
end
|
48
|
+
|
49
|
+
def row_param_prefix
|
50
|
+
self['row_param_prefix'] || 'r'
|
51
|
+
end
|
52
|
+
|
53
|
+
# Get the list of displayed fact attributes. If this value is not specified then all aggregate and calculated
|
54
|
+
# fields will be displayed
|
55
|
+
def fact_attributes
|
56
|
+
return self['fact_attributes'] if self['fact_attributes']
|
57
|
+
fa = []
|
58
|
+
fact_class.aggregate_fields.each do |name|
|
59
|
+
fa << name
|
60
|
+
end
|
61
|
+
fact_class.calculated_fields.each do |name|
|
62
|
+
fa << name
|
63
|
+
end
|
64
|
+
fa
|
65
|
+
end
|
66
|
+
|
67
|
+
def pass_params
|
68
|
+
@pass_params ||= []
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
# Callback which is invoked on each object returned from a call to the object's find method.
|
73
|
+
def after_find
|
74
|
+
from_storage
|
75
|
+
end
|
76
|
+
|
77
|
+
# Converts values for all columns which can store symbol values into strings. This is used to store
|
78
|
+
# the data in the database as a string rather than a YAML representation
|
79
|
+
def to_storage
|
80
|
+
symbol_attributes.each do |name|
|
81
|
+
self[name] = self[name].to_s if self[name]
|
82
|
+
end
|
83
|
+
list_attributes.each do |name|
|
84
|
+
self[name] = self[name].join(',') if self[name]
|
85
|
+
end
|
86
|
+
symbolized_list_attributes.each do |name|
|
87
|
+
self[name] = self[name].join(',') if self[name]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Converts values for all columns which store strings in the database to symbols.
|
92
|
+
def from_storage
|
93
|
+
symbol_attributes.each do |name|
|
94
|
+
self[name] = self[name].to_sym if self[name]
|
95
|
+
end
|
96
|
+
list_attributes.each do |name|
|
97
|
+
self[name] = self[name].split(/,/) if self[name]
|
98
|
+
end
|
99
|
+
symbolized_list_attributes.each do |name|
|
100
|
+
self[name] = self[name].split(/,/).collect { |v| v.to_sym } if self[name]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Attributes which should contain a symbol
|
105
|
+
def symbol_attributes
|
106
|
+
%w(cube_name column_dimension_name column_hierarchy row_dimension_name row_hierarchy)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Attributes which should contain a list of strings
|
110
|
+
def list_attributes
|
111
|
+
%w(column_constraints row_constraints)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Attributes which should contain a list of symbols
|
115
|
+
def symbolized_list_attributes
|
116
|
+
%w(fact_attributes)
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|