mainej-activewarehouse 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. data/activewarehouse/README +99 -0
  2. data/activewarehouse/Rakefile +165 -0
  3. data/activewarehouse/TODO +4 -0
  4. data/activewarehouse/db/migrations/001_create_table_reports.rb +28 -0
  5. data/activewarehouse/doc/references.txt +4 -0
  6. data/activewarehouse/generators/bridge/USAGE +1 -0
  7. data/activewarehouse/generators/bridge/bridge_generator.rb +46 -0
  8. data/activewarehouse/generators/bridge/templates/fixture.yml +5 -0
  9. data/activewarehouse/generators/bridge/templates/migration.rb +27 -0
  10. data/activewarehouse/generators/bridge/templates/model.rb +3 -0
  11. data/activewarehouse/generators/bridge/templates/unit_test.rb +8 -0
  12. data/activewarehouse/generators/cube/USAGE +1 -0
  13. data/activewarehouse/generators/cube/cube_generator.rb +28 -0
  14. data/activewarehouse/generators/cube/templates/model.rb +3 -0
  15. data/activewarehouse/generators/cube/templates/unit_test.rb +8 -0
  16. data/activewarehouse/generators/date_dimension/USAGE +1 -0
  17. data/activewarehouse/generators/date_dimension/date_dimension_generator.rb +16 -0
  18. data/activewarehouse/generators/date_dimension/templates/fixture.yml +5 -0
  19. data/activewarehouse/generators/date_dimension/templates/migration.rb +31 -0
  20. data/activewarehouse/generators/date_dimension/templates/model.rb +3 -0
  21. data/activewarehouse/generators/date_dimension/templates/unit_test.rb +8 -0
  22. data/activewarehouse/generators/dimension/USAGE +1 -0
  23. data/activewarehouse/generators/dimension/dimension_generator.rb +46 -0
  24. data/activewarehouse/generators/dimension/templates/fixture.yml +5 -0
  25. data/activewarehouse/generators/dimension/templates/migration.rb +11 -0
  26. data/activewarehouse/generators/dimension/templates/model.rb +3 -0
  27. data/activewarehouse/generators/dimension/templates/unit_test.rb +8 -0
  28. data/activewarehouse/generators/dimension_view/USAGE +1 -0
  29. data/activewarehouse/generators/dimension_view/dimension_view_generator.rb +62 -0
  30. data/activewarehouse/generators/dimension_view/templates/migration.rb +17 -0
  31. data/activewarehouse/generators/dimension_view/templates/model.rb +3 -0
  32. data/activewarehouse/generators/dimension_view/templates/unit_test.rb +10 -0
  33. data/activewarehouse/generators/fact/USAGE +1 -0
  34. data/activewarehouse/generators/fact/fact_generator.rb +46 -0
  35. data/activewarehouse/generators/fact/templates/fixture.yml +5 -0
  36. data/activewarehouse/generators/fact/templates/migration.rb +13 -0
  37. data/activewarehouse/generators/fact/templates/model.rb +3 -0
  38. data/activewarehouse/generators/fact/templates/unit_test.rb +10 -0
  39. data/activewarehouse/generators/time_dimension/USAGE +1 -0
  40. data/activewarehouse/generators/time_dimension/templates/fixture.yml +5 -0
  41. data/activewarehouse/generators/time_dimension/templates/migration.rb +12 -0
  42. data/activewarehouse/generators/time_dimension/templates/model.rb +3 -0
  43. data/activewarehouse/generators/time_dimension/templates/unit_test.rb +8 -0
  44. data/activewarehouse/generators/time_dimension/time_dimension_generator.rb +14 -0
  45. data/activewarehouse/init.rb +1 -0
  46. data/activewarehouse/install.rb +5 -0
  47. data/activewarehouse/lib/active_warehouse.rb +91 -0
  48. data/activewarehouse/lib/active_warehouse/aggregate.rb +75 -0
  49. data/activewarehouse/lib/active_warehouse/aggregate/dwarf_aggregate.rb +369 -0
  50. data/activewarehouse/lib/active_warehouse/aggregate/dwarf_common.rb +44 -0
  51. data/activewarehouse/lib/active_warehouse/aggregate/dwarf_printer.rb +34 -0
  52. data/activewarehouse/lib/active_warehouse/aggregate/no_aggregate.rb +212 -0
  53. data/activewarehouse/lib/active_warehouse/aggregate/pid_aggregate.rb +29 -0
  54. data/activewarehouse/lib/active_warehouse/aggregate_field.rb +59 -0
  55. data/activewarehouse/lib/active_warehouse/bridge.rb +19 -0
  56. data/activewarehouse/lib/active_warehouse/bridge/hierarchy_bridge.rb +46 -0
  57. data/activewarehouse/lib/active_warehouse/builder.rb +3 -0
  58. data/activewarehouse/lib/active_warehouse/builder/date_dimension_builder.rb +91 -0
  59. data/activewarehouse/lib/active_warehouse/builder/generator/generator.rb +13 -0
  60. data/activewarehouse/lib/active_warehouse/builder/generator/name_generator.rb +20 -0
  61. data/activewarehouse/lib/active_warehouse/builder/generator/paragraph_generator.rb +11 -0
  62. data/activewarehouse/lib/active_warehouse/builder/random_data_builder.rb +239 -0
  63. data/activewarehouse/lib/active_warehouse/builder/test_data_builder.rb +54 -0
  64. data/activewarehouse/lib/active_warehouse/calculated_field.rb +27 -0
  65. data/activewarehouse/lib/active_warehouse/compat/compat.rb +49 -0
  66. data/activewarehouse/lib/active_warehouse/core_ext.rb +1 -0
  67. data/activewarehouse/lib/active_warehouse/core_ext/time.rb +5 -0
  68. data/activewarehouse/lib/active_warehouse/core_ext/time/calculations.rb +40 -0
  69. data/activewarehouse/lib/active_warehouse/cube.rb +235 -0
  70. data/activewarehouse/lib/active_warehouse/cube_query_result.rb +69 -0
  71. data/activewarehouse/lib/active_warehouse/dimension.rb +329 -0
  72. data/activewarehouse/lib/active_warehouse/dimension/date_dimension.rb +15 -0
  73. data/activewarehouse/lib/active_warehouse/dimension/dimension_reflection.rb +21 -0
  74. data/activewarehouse/lib/active_warehouse/dimension/dimension_view.rb +27 -0
  75. data/activewarehouse/lib/active_warehouse/dimension/hierarchical_dimension.rb +99 -0
  76. data/activewarehouse/lib/active_warehouse/dimension/slowly_changing_dimension.rb +147 -0
  77. data/activewarehouse/lib/active_warehouse/fact.rb +239 -0
  78. data/activewarehouse/lib/active_warehouse/field.rb +74 -0
  79. data/activewarehouse/lib/active_warehouse/migrations.rb +64 -0
  80. data/activewarehouse/lib/active_warehouse/ordered_hash.rb +34 -0
  81. data/activewarehouse/lib/active_warehouse/prejoin_fact.rb +97 -0
  82. data/activewarehouse/lib/active_warehouse/report.rb +7 -0
  83. data/activewarehouse/lib/active_warehouse/report/abstract_report.rb +149 -0
  84. data/activewarehouse/lib/active_warehouse/report/chart_report.rb +9 -0
  85. data/activewarehouse/lib/active_warehouse/report/data_cell.rb +21 -0
  86. data/activewarehouse/lib/active_warehouse/report/data_column.rb +19 -0
  87. data/activewarehouse/lib/active_warehouse/report/data_row.rb +15 -0
  88. data/activewarehouse/lib/active_warehouse/report/dimension.rb +58 -0
  89. data/activewarehouse/lib/active_warehouse/report/table_report.rb +38 -0
  90. data/activewarehouse/lib/active_warehouse/version.rb +9 -0
  91. data/activewarehouse/lib/active_warehouse/view.rb +9 -0
  92. data/activewarehouse/lib/active_warehouse/view/crumb.rb +64 -0
  93. data/activewarehouse/lib/active_warehouse/view/report_helper.rb +98 -0
  94. data/activewarehouse/lib/active_warehouse/view/table_view.rb +134 -0
  95. data/activewarehouse/lib/active_warehouse/view/yui_adapter.rb +68 -0
  96. data/activewarehouse/tasks/active_warehouse_tasks.rake +122 -0
  97. metadata +237 -0
@@ -0,0 +1,235 @@
1
+ module ActiveWarehouse
2
+ # A Cube represents a collection of dimensions operating on a fact. The Cube
3
+ # provides a front-end for getting at the
4
+ # underlying data. Cubes support pluggable aggregation. The default aggregation
5
+ # is the NoAggregate which goes directly
6
+ # to the fact and dimensions to answer queries.
7
+ class Cube
8
+ class << self
9
+
10
+ # Callback which is invoked when subclasses are created
11
+ def inherited(subclass)
12
+ subclasses << subclass
13
+ end
14
+
15
+ # Get a list of all known subclasses
16
+ def subclasses
17
+ @subclasses ||= []
18
+ end
19
+
20
+ # Defines the dimensions that this cube pivots on. If the fact name and
21
+ # cube name are different (for example, if a PurchaseCube does not report
22
+ # on a PurchaseFact) then you *must* declare the <code>reports_on</code>
23
+ # first.
24
+ def pivots_on(*dimension_list)
25
+ @dimensions_hierarchies = OrderedHash.new
26
+ @dimensions = []
27
+ dimension_list.each do |dimension|
28
+ case dimension
29
+ when Symbol, String
30
+ dimensions << dimension.to_sym
31
+ dimensions_hierarchies[dimension.to_sym] = fact_class.dimension_class(dimension).hierarchies
32
+ when Hash
33
+ dimension_name = dimension.keys.first.to_sym
34
+ dimensions << dimension_name
35
+ dimensions_hierarchies[dimension_name] = [dimension[dimension_name]].flatten
36
+ else
37
+ raise ArgumentError, "Each argument to pivot_on must be a symbol, string or Hash"
38
+ end
39
+ end
40
+ end
41
+ alias :pivot_on :pivots_on
42
+
43
+ # Defines the fact name, without the 'Fact' suffix, that this cube
44
+ # reports on. For instance, if you have PurchaseFact, you could then
45
+ # call <code>reports_on :purchase</code>.
46
+ #
47
+ # The default value for reports_on is to take the name of the cube,
48
+ # i.e. PurchaseCube, and remove the Cube suffix. The assumption is that
49
+ # your Cube name matches your Fact name.
50
+ def reports_on(fact_name)
51
+ @fact_name = fact_name
52
+ end
53
+ alias :report_on :reports_on
54
+
55
+ # Rebuild the data warehouse.
56
+ def rebuild(options={})
57
+ populate(options)
58
+ end
59
+
60
+ # Populate the data warehouse. Delegate to aggregate.populate
61
+ def populate(options={})
62
+ aggregate.populate
63
+ end
64
+
65
+ # Get the dimensions that this cube pivots on
66
+ def dimensions
67
+ @dimensions ||= fact_class.dimension_relationships.collect{|k,v| k}
68
+ end
69
+
70
+ # Get an OrderedHash of each dimension mapped to its hierarchies which
71
+ # will be included in the cube
72
+ def dimensions_hierarchies
73
+ if @dimensions_hierarchies.nil?
74
+ @dimensions_hierarchies = OrderedHash.new
75
+ dimensions.each do |dimension|
76
+ @dimensions_hierarchies[dimension] = fact_class.dimension_class(dimension).hierarchies
77
+ end
78
+ end
79
+ @dimensions_hierarchies
80
+ end
81
+
82
+ # returns true if this cube pivots on a hierarchical dimension.
83
+ def pivot_on_hierarchical_dimension?
84
+ dimension_classes.each do |dimension|
85
+ return true if dimension.hierarchical_dimension?
86
+ end
87
+ return false
88
+ end
89
+
90
+ # returns the aggregate fields for this cube
91
+ # removing the aggregate fields that are defined in fact class that are
92
+ # related to hierarchical dimension, but the cube doesn't pivot on any
93
+ # hierarchical dimensions
94
+ # The method also further removes the not appropreate aggregate fields
95
+ # for the type of dimensions passed in if they exists.
96
+ def aggregate_fields(dims=[])
97
+ agg_fields = fact_class.aggregate_fields.reject {|field| !pivot_on_hierarchical_dimension? and !field.levels_from_parent.empty? }
98
+ dims.each do |dim|
99
+ if !dim.blank? and fact_class.has_and_belongs_to_many_relationship?(dim.to_sym)
100
+ return agg_fields.reject {|field| !field.is_count_distinct?}
101
+ end
102
+ end
103
+ agg_fields
104
+ end
105
+
106
+ def calculated_fields
107
+ fact_class.calculated_fields
108
+ end
109
+
110
+ def fields(dims=[])
111
+ aggregate_fields(dims) + calculated_fields
112
+ end
113
+
114
+ def field_names(dims=[])
115
+ fields(dims).map { |field| field.name.to_sym }
116
+ end
117
+
118
+ def fields_for_select(dims=[])
119
+ fields(dims).inject({}) { |select_hash, field| select_hash.update field.label.to_s => field.name.to_sym }
120
+ end
121
+
122
+ # Get the class name for the specified cube name
123
+ # Example: Regional Sales will become RegionalSalesCube
124
+ def class_name(name)
125
+ cube_name = name.to_s
126
+ cube_name = "#{cube_name}_cube" unless cube_name =~ /_cube$/
127
+ cube_name.classify
128
+ end
129
+
130
+ # Get the aggregated fact class name
131
+ def fact_class_name
132
+ ActiveWarehouse::Fact.class_name(@fact_name || name.sub(/Cube$/,'').underscore.to_sym)
133
+ end
134
+
135
+ # Get the aggregated fact class instance
136
+ def fact_class
137
+ fact_class_name.constantize
138
+ end
139
+
140
+ # Get a list of dimension class instances
141
+ def dimension_classes
142
+ dimensions.collect do |dimension_name|
143
+ dimension_class(dimension_name)
144
+ end
145
+ end
146
+
147
+ # Get the dimension class for the specified dimension name
148
+ def dimension_class(dimension_name)
149
+ fact_class.dimension_relationships[dimension_name.to_sym].class_name.constantize
150
+ end
151
+
152
+ # Get the cube logger
153
+ def logger
154
+ @logger ||= Logger.new('cube.log')
155
+ end
156
+
157
+ # Get the time when the fact or any dimension referenced in this cube
158
+ # was last modified
159
+ def last_modified
160
+ lm = fact_class.last_modified
161
+ dimensions.each do |dimension|
162
+ dim = ActiveWarehouse::Dimension.class_for_name(dimension)
163
+ lm = dim.last_modified if dim.last_modified > lm
164
+ end
165
+ lm
166
+ end
167
+
168
+ # The temp directory for storing files during warehouse rebuilds
169
+ attr_accessor :temp_dir
170
+ def temp_dir
171
+ @temp_dir ||= '/tmp'
172
+ end
173
+
174
+ # Specify the ActiveRecord class to connect through
175
+ # Note: this is a potential directive in a Cube subclass
176
+ attr_accessor :connect_through
177
+ def connect_through
178
+ @connect_through ||= ActiveRecord::Base
179
+ end
180
+
181
+ # Get an adapter connection
182
+ def connection
183
+ connect_through.connection
184
+ end
185
+
186
+ # Defaults to NoAggregate strategy.
187
+ def aggregate
188
+ @aggregate ||= ActiveWarehouse::Aggregate::NoAggregate.new(self)
189
+ end
190
+
191
+ def aggregate_class(agg_class)
192
+ @aggregate = agg_class.new(self)
193
+ end
194
+
195
+ end
196
+
197
+ public
198
+ # Query the cube. The column dimension, column hierarchy, row dimension and
199
+ # row hierarchy are all required.
200
+ #
201
+ # The conditions value is a String that represents a SQL condition appended
202
+ # to the where clause. TODO: this may eventually be converted to another
203
+ # query language.
204
+ #
205
+ # The cstage value represents the current column drill down stage and
206
+ # defaults to 0.
207
+ #
208
+ # The rstage value represents the current row drill down stage and defaults
209
+ # to 0. Filters contains key/value pairs where the key is a string of
210
+ # 'dimension.column' and the value is the value to filter by. For example:
211
+ #
212
+ # filters = {'date.calendar_year' => 2007, 'product.category' => 'Food'}
213
+ # query(:date, :cy, :store, :region, 1, 0, filters)
214
+ #
215
+ # Note that product.category refers to a dimension which is not actually
216
+ # visible but which is both part of the cube and is used for filtering.
217
+ def query(*args)
218
+ self.class.aggregate.query(*args)
219
+ end
220
+
221
+ # Similar to query, but expects slightly different arguments. See
222
+ # ActiveWarehouse::Aggregate::Aggregate.query_row_and_column for
223
+ # details.
224
+ def query_row_and_column(*args)
225
+ self.class.aggregate.query_row_and_column(*args)
226
+ end
227
+
228
+ # Get the database connection (delegates to Cube.connection class method)
229
+ def connection
230
+ self.class.connection
231
+ end
232
+
233
+ end
234
+
235
+ end
@@ -0,0 +1,69 @@
1
+ module ActiveWarehouse #:nodoc:
2
+ # Class that holds the results of a Cube query
3
+ class CubeQueryResult
4
+ attr_reader :aggregate_fields_hash
5
+
6
+ # Initialize the aggregate map with an array of AggregateField instances.
7
+ # The AggregateFields are used to typecast the raw values coming from
8
+ # the database. Thank you very little, DBI.
9
+ def initialize(aggregate_fields)
10
+ raise ArgumentError, "aggregate_fields must not be empty" unless aggregate_fields && aggregate_fields.size > 0
11
+ @aggregate_fields_hash = {}
12
+ aggregate_fields.each {|c| @aggregate_fields_hash[c.label] = c}
13
+ @values_map = {}
14
+ end
15
+
16
+ # Return true if the aggregate map includes the specified row value
17
+ def has_row_values?(row_value)
18
+ @values_map.has_key?(row_value.to_s)
19
+ end
20
+
21
+ # iterate through every row and column combination
22
+ def each
23
+ @values_map.each do |key, value|
24
+ yield key, value
25
+ end
26
+ end
27
+
28
+ def value(row_value, col_value, field_label)
29
+ #puts "getting value #{row_value},#{col_value},#{field_label}"
30
+ values(row_value, col_value)[field_label]
31
+ end
32
+
33
+ # returns a hash of type casted fact values for the intersection of
34
+ # row_value and col_value
35
+ def values(row_value, col_value)
36
+ row = @values_map[row_value.to_s]
37
+ return empty_hash_for_missing_row_or_column if row.nil?
38
+ facts = row[col_value.to_s]
39
+ return empty_hash_for_missing_row_or_column if facts.nil?
40
+ facts
41
+ end
42
+
43
+ # Add a hash of aggregated facts for the given row and column values.
44
+ # For instance, add_data('Southeast', 2005, {:sales_sum => 40000, :sales_count => 40})
45
+ # This method will typecast the values in aggregated_facts.
46
+ def add_data(row_value, col_value, aggregated_facts)
47
+ #puts "Adding data for #{row_value}, #{col_value} [data=[#{aggregated_facts.join(',')}]]"
48
+ @values_map[row_value.to_s] ||= {}
49
+ @values_map[row_value.to_s][col_value.to_s] = typecast_facts(aggregated_facts)
50
+ end
51
+
52
+ private
53
+ def empty_hash_for_missing_row_or_column
54
+ empty = {}
55
+ aggregate_fields_hash.keys.each {|k| empty[k] = 0}
56
+ empty
57
+ end
58
+
59
+ def typecast_facts(raw_facts)
60
+ raw_facts.each do |k,v|
61
+ field = aggregate_fields_hash[k]
62
+ if field.nil?
63
+ raise ArgumentError, "'#{k}' is an unknown aggregate field in this query result"
64
+ end
65
+ raw_facts[k] = field.type_cast(v)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,329 @@
1
+ # require all of the "acts_as" mixins first
2
+ require 'active_warehouse/dimension/hierarchical_dimension'
3
+ require 'active_warehouse/dimension/slowly_changing_dimension'
4
+ require 'active_warehouse/dimension/dimension_reflection'
5
+
6
+ ActiveRecord::Reflection::AssociationReflection.send(:include, ActiveWarehouse::DimensionReflection)
7
+
8
+ module ActiveWarehouse #:nodoc
9
+ # Dimension tables contain the textual descriptors of the business. Dimensions
10
+ # provide the filters which are applied to facts. Dimensions are the primary
11
+ # source of query constraints, groupings and report labels.
12
+ class Dimension < ActiveRecord::Base
13
+ include ActiveWarehouse::HierarchicalDimension
14
+ include ActiveWarehouse::SlowlyChangingDimension
15
+
16
+ after_save :expire_value_tree_cache
17
+
18
+ class << self
19
+ # Alternate order by, to be used rather than the current level being queried
20
+ attr_accessor :order
21
+
22
+ # Map of level names to alternate order columns
23
+ attr_reader :level_orders
24
+
25
+ # Define a column to order by. If this value is specified then it will be
26
+ # used rather than the actual level being queried in the following method
27
+ # calls:
28
+ # * available_values
29
+ # * available_child_values
30
+ # * available_values_tree
31
+ def set_order(name)
32
+ @order = name
33
+ end
34
+
35
+ # Define a column to order by for a specific level.
36
+ def set_level_order(level, name)
37
+ level_orders[level] = name
38
+ end
39
+
40
+ # Get the level orders map
41
+ def level_orders
42
+ @level_orders ||= {}
43
+ end
44
+
45
+ # Define a named attribute hierarchy in the dimension.
46
+ #
47
+ # Example: define_hierarchy(:fiscal_calendar, [:fiscal_year, :fiscal_quarter, :fiscal_month])
48
+ #
49
+ # This would indicate that one of the drill down paths for this dimension is:
50
+ # Fiscal Year -> Fiscal Quarter -> Fiscal Month
51
+ #
52
+ # Internally the hierarchies are stored in order. The first hierarchy
53
+ # defined will be used as the default if no hierarchy is specified when
54
+ # rendering a cube.
55
+ def define_hierarchy(name, levels)
56
+ hierarchies << name
57
+ hierarchy_levels[name] = levels
58
+ end
59
+
60
+ # Get the named attribute hierarchy. Returns an array of column names.
61
+ #
62
+ # Example: hierarchy(:fiscal_calendar) might return [:fiscal_year, :fiscal_quarter, :fiscal_month]
63
+ def hierarchy(name)
64
+ hierarchy_levels[name]
65
+ end
66
+
67
+ # Get the ordered hierarchy names
68
+ def hierarchies
69
+ @hierarchies ||= []
70
+ end
71
+
72
+ # Get the hierarchy levels hash
73
+ def hierarchy_levels
74
+ @hierarchy_levels ||= {}
75
+ end
76
+
77
+ # Return a symbol used when referring to this dimension. The symbol is
78
+ # calculated by demodulizing and underscoring the
79
+ # dimension's class name and then removing the trailing _dimension.
80
+ #
81
+ # Example: DateDimension will return a symbol :date
82
+ def sym
83
+ self.name.demodulize.underscore.gsub(/_dimension/, '').to_sym
84
+ end
85
+
86
+ # Get the table name. By default the table name will be the name of the
87
+ # dimension in singular form.
88
+ #
89
+ # Example: DateDimension will have a table called date_dimension
90
+ def table_name
91
+ name = self.name.demodulize.underscore
92
+ set_table_name(name)
93
+ name
94
+ end
95
+
96
+ # Convert the given name into a dimension class name
97
+ def class_name(name)
98
+ dimension_name = name.to_s
99
+ dimension_name = "#{dimension_name}_dimension" unless dimension_name =~ /_dimension$/
100
+ dimension_name.classify
101
+ end
102
+
103
+ # Get a class for the specified named dimension
104
+ def class_for_name(name)
105
+ class_name(name).constantize
106
+ end
107
+
108
+ # Return the time when the underlying dimension source file was last
109
+ # modified. This is used
110
+ # to determine if a cube structure rebuild is required
111
+ def last_modified
112
+ File.new(__FILE__).mtime
113
+ end
114
+
115
+ # Get the dimension class for the specified dimension parameter. The
116
+ # dimension parameter may be a class, String or Symbol.
117
+ def to_dimension(dimension)
118
+ return dimension if dimension.is_a?(Class) and dimension.ancestors.include?(Dimension)
119
+ return class_for_name(dimension)
120
+ end
121
+
122
+ # Returns a hash of all of the values at the specified hierarchy level
123
+ # mapped to the count at that level. For example, given a date dimension
124
+ # with years from 2002 to 2004 and a hierarchy defined with:
125
+ #
126
+ # hierarchy :cy, [:calendar_year, :calendar_quarter, :calendar_month_name]
127
+ #
128
+ # ...then...
129
+ #
130
+ # DateDimension.denominator_count(:cy, :calendar_year, :calendar_quarter)
131
+ # returns {'2002' => 4, '2003' => 4, '2004' => 4}
132
+ #
133
+ # If the denominator_level parameter is omitted or nil then:
134
+ #
135
+ # DateDimension.denominator_count(:cy, :calendar_year) returns
136
+ # {'2003' => 365, '2003' => 365, '2004' => 366}
137
+ #
138
+ def denominator_count(hierarchy_name, level, denominator_level=nil)
139
+ if hierarchy_levels[hierarchy_name].nil?
140
+ raise ArgumentError, "The hierarchy '#{hierarchy_name}' does not exist in your dimension #{name}"
141
+ end
142
+
143
+ q = nil
144
+ # If the denominator_level is specified and it is not the last element
145
+ # in the hierarchy then do a distinct count. If
146
+ # the denominator level is less than the current level then raise an
147
+ # ArgumentError. In other words, if the current level is
148
+ # calendar month then passing in calendar year as the denominator level
149
+ # would raise an ArgumentErro.
150
+ #
151
+ # If the denominator_level is not specified then assume the finest grain
152
+ # possible (in the context of a date dimension this would be each day)
153
+ # and use the id to count.
154
+ if denominator_level && hierarchy_levels[hierarchy_name].last != denominator_level
155
+ level_index = hierarchy_levels[hierarchy_name].index(level)
156
+ denominator_index = hierarchy_levels[hierarchy_name].index(denominator_level)
157
+
158
+ if level_index.nil?
159
+ raise ArgumentError, "The level '#{level}' does not appear to exist"
160
+ end
161
+ if denominator_index.nil?
162
+ raise ArgumentError, "The denominator level '#{denominator_level}' does not appear to exist"
163
+ end
164
+ if hierarchy_levels[hierarchy_name].index(denominator_level) < hierarchy_levels[hierarchy_name].index(level)
165
+ 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}'"
166
+ end
167
+
168
+ q = "select #{level} as level, count(distinct(#{denominator_level})) as level_count from #{table_name} group by #{level}"
169
+ else
170
+ q = "select #{level} as level, count(id) as level_count from #{table_name} group by #{level}"
171
+ end
172
+ denominators = {}
173
+ connection.select_all(q).each do |row|
174
+ denominators[row['level']] = row['level_count'].to_i
175
+ end
176
+ denominators
177
+ end
178
+
179
+ # Get the foreign key for this dimension which is used in Fact tables.
180
+ #
181
+ # Example: DateDimension would have a foreign key of date_id
182
+ #
183
+ # The actual foreign key may be different and depends on the fact class.
184
+ # You may specify the foreign key to use for a specific fact using the
185
+ # Fact#set_dimension_options method.
186
+ def foreign_key
187
+ table_name.sub(/_dimension/,'') + '_id'
188
+ end
189
+
190
+ # Get an array of the available values for a particular hierarchy level
191
+ # For example, given a DateDimension with data from 2002 to 2004:
192
+ #
193
+ # available_values('calendar_year') returns ['2002','2003','2004']
194
+ def available_values(level)
195
+ level_method = level.to_sym
196
+ level = connection.quote_column_name(level.to_s)
197
+ order = level_orders[level] || self.order || level
198
+
199
+ options = {:select => "distinct #{order.to_s == level.to_s ? '' : order.to_s+','} #{level}", :order => order}
200
+ values = []
201
+ find(:all, options).each do |dim|
202
+ value = dim.send(level_method)
203
+ values << dim.send(level_method) unless values.include?(value)
204
+ end
205
+ values.to_a
206
+ end
207
+
208
+ # Get an array of child values for a particular parent in the hierachy
209
+ # For example, given a DateDimension with data from 2002 to 2004:
210
+ #
211
+ # available_child_values(:cy, [2002, 'Q1']) returns
212
+ # ['January', 'Feburary', 'March', 'April']
213
+ def available_child_values(hierarchy_name, parent_values)
214
+ if hierarchy_levels[hierarchy_name].nil?
215
+ raise ArgumentError, "The hierarchy '#{hierarchy_name}' does not exist in your dimension #{name}"
216
+ end
217
+
218
+ levels = hierarchy_levels[hierarchy_name]
219
+ if levels.length <= parent_values.length
220
+ raise ArgumentError, "The parent_values '#{parent_values.to_yaml}' exceeds the hierarchy depth #{levels.to_yaml}"
221
+ end
222
+
223
+ child_level = levels[parent_values.length].to_s
224
+
225
+ # Create the conditions array. Will work with 1.1.6.
226
+ conditions_parts = []
227
+ conditions_values = []
228
+ parent_values.each_with_index do |value, index|
229
+ conditions_parts << "#{levels[index]} = ?"
230
+ conditions_values << value
231
+ end
232
+ conditions = [conditions_parts.join(' AND ')] + conditions_values unless conditions_parts.empty?
233
+
234
+ child_level_method = child_level.to_sym
235
+ child_level = connection.quote_column_name(child_level)
236
+ order = level_orders[child_level] || self.order || child_level
237
+
238
+ select_sql = "distinct #{child_level}"
239
+ select_sql += ", #{order}" unless order == child_level
240
+ options = {:select => select_sql, :order => order}
241
+
242
+ options[:conditions] = conditions unless conditions.nil?
243
+
244
+ find(:all, options).map do |dim|
245
+ dim.send(child_level_method)
246
+ end.uniq
247
+ end
248
+ alias :available_children_values :available_child_values
249
+
250
+ # Get a tree of Node objects for all of the values in the specified hierarchy.
251
+ def available_values_tree(hierarchy_name)
252
+ root = value_tree_cache[hierarchy_name]
253
+ if root.nil?
254
+ root = Node.new('All', '__ROOT__')
255
+ levels = hierarchy(hierarchy_name)
256
+ nodes = {nil => root}
257
+ level_list = levels.collect{|level| connection.quote_column_name(level) }.join(',')
258
+ order = self.order || level_list
259
+ find(:all, :select => level_list, :group => level_list, :order => order).each do |dim|
260
+ parent_node = root
261
+ levels.each do |level|
262
+ node_value = dim.send(level)
263
+ child_node = parent_node.optionally_add_child(node_value, level)
264
+ parent_node = child_node
265
+ end
266
+ end
267
+ value_tree_cache[hierarchy_name] = root
268
+ end
269
+ root
270
+ end
271
+
272
+ protected
273
+ # Get the value tree cache
274
+ def value_tree_cache
275
+ @value_tree_cache ||= {}
276
+ end
277
+
278
+ class Node#:nodoc:
279
+ attr_reader :value, :children, :parent, :level
280
+
281
+ def initialize(value, level, parent = nil)
282
+ @children = []
283
+ @value = value
284
+ @parent = parent
285
+ @level = level
286
+ end
287
+
288
+ def has_child?(child_value)
289
+ !self.child(child_value).nil?
290
+ end
291
+
292
+ def child(child_value)
293
+ @children.each do |c|
294
+ return c if c.value == child_value
295
+ end
296
+ nil
297
+ end
298
+
299
+ def add_child(child_value, level)
300
+ child = Node.new(child_value, level, self)
301
+ @children << child
302
+ child
303
+ end
304
+
305
+ def optionally_add_child(child_value, level)
306
+ c = child(child_value)
307
+ c = add_child(child_value, level) unless c
308
+ c
309
+ end
310
+ end
311
+
312
+ public
313
+ # Expire the value tree cache. This should be called if the dimension
314
+ def expire_value_tree_cache
315
+ @value_tree_cache = nil
316
+ end
317
+ end
318
+
319
+ public
320
+ # Expire the value tree cache
321
+ def expire_value_tree_cache
322
+ self.class.expire_value_tree_cache
323
+ end
324
+
325
+ end
326
+ end
327
+
328
+ require 'active_warehouse/dimension/date_dimension'
329
+ require 'active_warehouse/dimension/dimension_view'