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