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,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
|