activewarehouse 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/README +41 -0
  2. data/Rakefile +121 -0
  3. data/TODO +4 -0
  4. data/db/migrations/001_create_table_reports.rb +28 -0
  5. data/doc/agg_queries.txt +26 -0
  6. data/doc/agg_queries_results.txt +150 -0
  7. data/doc/queries.txt +35 -0
  8. data/generators/cube/USAGE +1 -0
  9. data/generators/cube/cube_generator.rb +28 -0
  10. data/generators/cube/templates/model.rb +3 -0
  11. data/generators/cube/templates/unit_test.rb +8 -0
  12. data/generators/dimension/USAGE +1 -0
  13. data/generators/dimension/dimension_generator.rb +46 -0
  14. data/generators/dimension/templates/fixture.yml +5 -0
  15. data/generators/dimension/templates/migration.rb +20 -0
  16. data/generators/dimension/templates/model.rb +3 -0
  17. data/generators/dimension/templates/unit_test.rb +10 -0
  18. data/generators/fact/USAGE +1 -0
  19. data/generators/fact/fact_generator.rb +46 -0
  20. data/generators/fact/templates/fixture.yml +5 -0
  21. data/generators/fact/templates/migration.rb +11 -0
  22. data/generators/fact/templates/model.rb +3 -0
  23. data/generators/fact/templates/unit_test.rb +10 -0
  24. data/install.rb +5 -0
  25. data/lib/active_warehouse.rb +65 -0
  26. data/lib/active_warehouse/builder.rb +2 -0
  27. data/lib/active_warehouse/builder/date_dimension_builder.rb +65 -0
  28. data/lib/active_warehouse/builder/random_data_builder.rb +13 -0
  29. data/lib/active_warehouse/core_ext.rb +1 -0
  30. data/lib/active_warehouse/core_ext/time.rb +5 -0
  31. data/lib/active_warehouse/core_ext/time/calculations.rb +40 -0
  32. data/lib/active_warehouse/migrations.rb +65 -0
  33. data/lib/active_warehouse/model.rb +5 -0
  34. data/lib/active_warehouse/model/aggregate.rb +244 -0
  35. data/lib/active_warehouse/model/cube.rb +273 -0
  36. data/lib/active_warehouse/model/dimension.rb +3 -0
  37. data/lib/active_warehouse/model/dimension/bridge.rb +32 -0
  38. data/lib/active_warehouse/model/dimension/dimension.rb +152 -0
  39. data/lib/active_warehouse/model/dimension/hierarchical_dimension.rb +35 -0
  40. data/lib/active_warehouse/model/fact.rb +96 -0
  41. data/lib/active_warehouse/model/report.rb +3 -0
  42. data/lib/active_warehouse/model/report/abstract_report.rb +121 -0
  43. data/lib/active_warehouse/model/report/chart_report.rb +9 -0
  44. data/lib/active_warehouse/model/report/table_report.rb +23 -0
  45. data/lib/active_warehouse/version.rb +9 -0
  46. data/lib/active_warehouse/view.rb +2 -0
  47. data/lib/active_warehouse/view/report_helper.rb +213 -0
  48. data/tasks/active_warehouse_tasks.rake +50 -0
  49. metadata +144 -0
@@ -0,0 +1,3 @@
1
+ require 'active_warehouse/model/dimension/dimension'
2
+ require 'active_warehouse/model/dimension/hierarchical_dimension'
3
+ require 'active_warehouse/model/dimension/bridge'
@@ -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,3 @@
1
+ require 'active_warehouse/model/report/abstract_report'
2
+ require 'active_warehouse/model/report/table_report'
3
+ require 'active_warehouse/model/report/chart_report'
@@ -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