activewarehouse 0.1.0 → 0.2.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 (44) hide show
  1. data/README +62 -17
  2. data/Rakefile +17 -0
  3. data/generators/bridge/USAGE +1 -0
  4. data/generators/bridge/bridge_generator.rb +46 -0
  5. data/generators/bridge/templates/fixture.yml +5 -0
  6. data/generators/bridge/templates/migration.rb +20 -0
  7. data/generators/bridge/templates/model.rb +3 -0
  8. data/generators/dimension/templates/unit_test.rb +0 -2
  9. data/generators/dimension_view/USAGE +1 -0
  10. data/generators/dimension_view/dimension_view_generator.rb +62 -0
  11. data/generators/dimension_view/templates/migration.rb +11 -0
  12. data/generators/dimension_view/templates/model.rb +3 -0
  13. data/generators/dimension_view/templates/unit_test.rb +10 -0
  14. data/init.rb +1 -0
  15. data/lib/active_warehouse.rb +24 -9
  16. data/lib/active_warehouse/{model/aggregate.rb → aggregate.rb} +29 -13
  17. data/lib/active_warehouse/builder/date_dimension_builder.rb +21 -6
  18. data/lib/active_warehouse/builder/random_data_builder.rb +204 -3
  19. data/lib/active_warehouse/compat/compat.rb +49 -0
  20. data/lib/active_warehouse/{model/cube.rb → cube.rb} +47 -17
  21. data/lib/active_warehouse/dimension.rb +296 -0
  22. data/lib/active_warehouse/dimension/bridge.rb +15 -0
  23. data/lib/active_warehouse/dimension/dimension_view.rb +11 -0
  24. data/lib/active_warehouse/dimension/hierarchical_dimension.rb +60 -0
  25. data/lib/active_warehouse/dimension/slowly_changing_dimension.rb +137 -0
  26. data/lib/active_warehouse/{model/fact.rb → fact.rb} +45 -10
  27. data/lib/active_warehouse/migrations.rb +1 -2
  28. data/lib/active_warehouse/report.rb +3 -0
  29. data/lib/active_warehouse/{model/report → report}/abstract_report.rb +0 -0
  30. data/lib/active_warehouse/{model/report → report}/chart_report.rb +0 -0
  31. data/lib/active_warehouse/{model/report → report}/table_report.rb +0 -0
  32. data/lib/active_warehouse/version.rb +2 -2
  33. data/lib/active_warehouse/view/report_helper.rb +2 -1
  34. data/tasks/active_warehouse_tasks.rake +54 -0
  35. metadata +43 -21
  36. data/doc/agg_queries.txt +0 -26
  37. data/doc/agg_queries_results.txt +0 -150
  38. data/doc/queries.txt +0 -35
  39. data/lib/active_warehouse/model.rb +0 -5
  40. data/lib/active_warehouse/model/dimension.rb +0 -3
  41. data/lib/active_warehouse/model/dimension/bridge.rb +0 -32
  42. data/lib/active_warehouse/model/dimension/dimension.rb +0 -152
  43. data/lib/active_warehouse/model/dimension/hierarchical_dimension.rb +0 -35
  44. data/lib/active_warehouse/model/report.rb +0 -3
@@ -1,18 +1,33 @@
1
- module ActiveWarehouse
2
- module Builder
1
+ module ActiveWarehouse #:nodoc:
2
+ module Builder #:nodoc:
3
+ # A builder which will build a data structure which can be used to populate a date dimension using
4
+ # commonly used date dimension columns.
3
5
  class DateDimensionBuilder
4
- attr_accessor :start_date, :end_date, :holiday_indicators
6
+ # Specify the start date for the first record
7
+ attr_accessor :start_date
8
+
9
+ # Specify the end date for the last record
10
+ attr_accessor :end_date
11
+
12
+ # Define any holiday indicators
13
+ attr_accessor :holiday_indicators
14
+
15
+ # Define the weekday indicators. The default array begins on Sunday and goes to Saturday.
5
16
  cattr_accessor :weekday_indicators
6
17
  @@weekday_indicators = ['Weekend','Weekday','Weekday','Weekday','Weekday','Weekday','Weekend']
7
-
18
+
19
+ # Initialize the builder.
20
+ #
21
+ # * <tt>start_date</tt>: The start date. Defaults to 5 years ago from today.
22
+ # * <tt>end_date</tt>: The end date. Defaults to now.
8
23
  def initialize(start_date=Time.now.years_ago(5), end_date=Time.now)
9
24
  @start_date = start_date
10
25
  @end_date = end_date
11
26
  @holiday_indicators = []
12
27
  end
13
28
 
14
- # Returns an array of hashes representing records in the dimension
15
- # The values for each record are accessed by name
29
+ # Returns an array of hashes representing records in the dimension. The values for each record are
30
+ # accessed by name.
16
31
  def build(options={})
17
32
  records = []
18
33
  date = start_date
@@ -1,12 +1,213 @@
1
- module ActiveWarehouse
2
- module Builder
1
+ module ActiveWarehouse #:nodoc:
2
+ module Builder #:nodoc:
3
+ # Build random data usable for testing.
3
4
  class RandomDataBuilder
5
+ # Hash of generators where the key is the class and the value is an implementation of AbstractGenerator
6
+ attr_reader :generators
7
+
8
+ # Hash of names mapped to generators where the name is the column name
9
+ attr_reader :column_generators
10
+
4
11
  def initialize
12
+ @generators = {
13
+ Fixnum => FixnumGenerator.new,
14
+ Float => FloatGenerator.new,
15
+ Date => DateGenerator.new,
16
+ Time => TimeGenerator.new,
17
+ String => StringGenerator.new,
18
+ Object => BooleanGenerator.new,
19
+ }
20
+ @column_generators = {}
21
+ end
22
+
23
+ def build(name, options={})
24
+ case name
25
+ when Class
26
+ if name.respond_to?(:base_class)
27
+ return build_dimension(name, options) if name.base_class == ActiveWarehouse::Dimension
28
+ return build_fact(name, options) if name.base_class == ActiveWarehouse::Fact
29
+ end
30
+ raise "#{name} is a class but does not appear to descend from Fact or Dimension"
31
+ when String
32
+ begin
33
+ build(name.classify.constantize, options)
34
+ rescue NameError
35
+ raise "Cannot find a class named #{name.classify}"
36
+ end
37
+ when Symbol
38
+ build(name.to_s, options)
39
+ else
40
+ raise "Unable to determine what to build"
41
+ end
42
+ end
43
+
44
+ # Build test dimension data for the specified dimension name.
45
+ #
46
+ # Options:
47
+ # * <tt>:rows</tt>: The number of rows to create (defaults to 100)
48
+ # * <tt>:generators</tt>: A map of generators where each key is Fixnum, Float, Date, Time, String, or Object and the
49
+ # value is extends from AbstractGenerator.
50
+ def build_dimension(name, options={})
51
+ options[:rows] ||= 100
52
+ options[:generators] ||= {}
53
+ rows = []
54
+
55
+ dimension_class = Dimension.to_dimension(name)
56
+ options[:rows].times do
57
+ row = {}
58
+ dimension_class.content_columns.each do |column|
59
+ generator = (options[:generators][column.klass] || @column_generators[column.name] || @generators[column.klass])
60
+ row[column.name] = generator.generate(column, options)
61
+ end
62
+ rows << row
63
+ end
5
64
 
65
+ rows
6
66
  end
7
67
 
8
- def build(options={})
68
+ # Build test fact data for the specified fact name
69
+ #
70
+ # Options:
71
+ # * <tt>:rows</tt>: The number of rows to create (defaults to 100)
72
+ # * <tt>:generators</tt>: A Hash of generators where each key is Fixnum, Float, Date, Time, String, or Object and the
73
+ # value is extends from AbstractGenerator.
74
+ # * <tt>:fk_limit</tt>: A Hash of foreign key limits, where each key is the name of column and the value is
75
+ # a number. For example options[:fk_limit][:date_id] = 1000 would limit the foreign key values to something between
76
+ # 1 and 1000, inclusive.
77
+ def build_fact(name, options={})
78
+ options[:rows] ||= 100
79
+ options[:generators] ||= {}
80
+ options[:fk_limit] ||= {}
81
+ rows = []
82
+
83
+ fact_class = Fact.to_fact(name)
84
+ options[:rows].times do
85
+ row = {}
86
+ fact_class.content_columns.each do |column|
87
+ generator = (options[:generators][column.klass] || @generators[column.klass])
88
+ row[column.name] = generator.generate(column, options)
89
+ end
90
+ fact_class.foreign_key_columns.each do |column|
91
+ fk_limit = (options[:fk_limit][column.name] || 100) - 1
92
+ row[column.name] = rand(fk_limit) + 1
93
+ end
94
+ rows << row
95
+ end
9
96
 
97
+ rows
98
+ end
99
+ end
100
+
101
+ # Implement this class to provide an generator implementation for a specific class.
102
+ class AbstractGenerator
103
+ # Generate the next value. The column parameter must be an ActiveRecord::Adapter::Column instance.
104
+ # The options hash is implementation dependent.
105
+ def generate(column, options={})
106
+ raise "generate method must be implemented by a subclass"
107
+ end
108
+ end
109
+
110
+ # Basic Date generator
111
+ class DateGenerator < AbstractGenerator
112
+ # Generate a random date value
113
+ #
114
+ # Options:
115
+ # *<tt>:start_date</tt>: The start date as a Date or Time object (default 1 year ago)
116
+ # *<tt>:end_date</tt>: The end date as a Date or Time object (default now)
117
+ def generate(column, options={})
118
+ end_date = (options[:end_date] || Time.now).to_date
119
+ start_date = (options[:start_date] || 1.year.ago).to_date
120
+ number_of_days = end_date - start_date
121
+ start_date + rand(number_of_days)
122
+ end
123
+ end
124
+
125
+ # Basic Time generator
126
+ #
127
+ # Options:
128
+ # *<tt>:start_date</tt>: The start date as a Date or Time object (default 1 year ago)
129
+ # *<tt>:end_date</tt>: The end date as a Date or Time object (default now)
130
+ class TimeGenerator < DateGenerator #:nodoc:
131
+ # Generate a random Time value
132
+ def generate(column, options={})
133
+ super(column, options).to_time
134
+ end
135
+ end
136
+
137
+ # Basic Fixnum generator
138
+ class FixnumGenerator
139
+ # Generate an integer from 0 to options[:max] inclusive
140
+ #
141
+ # Options:
142
+ # *<tt>:max</tt>: The maximum allowed value (default 1000)
143
+ # *<tt>:min</tt>: The minimum allowed value (default 0)
144
+ def generate(column, options={})
145
+ options[:max] ||= 1000
146
+ options[:min] ||= 0
147
+ rand(options[:max] + (-options[:min])) - options[:min]
148
+ end
149
+ end
150
+
151
+ # Basic Float generator
152
+ class FloatGenerator
153
+ # Generate a float from 0 to options[:max] inclusive (default 1000)
154
+ #
155
+ # Options:
156
+ # *<tt>:max</tt>: The maximum allowed value (default 1000)
157
+ def generate(column, options={})
158
+ options[:max] ||= 1000
159
+ rand * options[:max].to_f
160
+ end
161
+ end
162
+
163
+ # Basic BigDecimal generator
164
+ class BigDecimalGenerator
165
+ # Generate a big decimal from 0 to options[:max] inclusive (default 1000)
166
+ #
167
+ # Options:
168
+ # *<tt>:max</tt>: The maximum allowed value (default 1000)
169
+ def generate(column, options={})
170
+ options[:max] ||= 1000
171
+ BigDecimal.new((rand * options[:max].to_f).to_s) # TODO: need BigDecimal type?
172
+ end
173
+ end
174
+
175
+ # A basic String generator
176
+ class StringGenerator
177
+ # Initialize the StringGenerator.
178
+ #
179
+ # Options:
180
+ # * <tt>:values</tt>: List of possible values
181
+ # * <tt>:chars</tt>: List of chars to use to generate random values
182
+ def initialize(options={})
183
+ @options = options
184
+ end
185
+ # Generate a random string
186
+ #
187
+ # Options:
188
+ # * <tt>:values</tt>: An array of values to use. If not specified then random char values will be used.
189
+ # * <tt>:chars</tt>: An array of characters to use to generate random values (default [a..zA..Z])
190
+ def generate(column, options={})
191
+ options[:values] ||= @options[:values]
192
+ options[:chars] ||= @options[:chars]
193
+ if options[:values]
194
+ options[:values][rand(options[:values].length)]
195
+ else
196
+ s = ''
197
+ chars = (options[:chars] || ('a'..'z').to_a + ('A'..'Z').to_a)
198
+ 0.upto(column.limit - 1) do |n|
199
+ s << chars[rand(chars.length)]
200
+ end
201
+ s
202
+ end
203
+ end
204
+ end
205
+
206
+ # A basic Boolean generator
207
+ class BooleanGenerator
208
+ # Generate a random boolean
209
+ def generate(column, options={})
210
+ rand(1) == 1
10
211
  end
11
212
  end
12
213
  end
@@ -0,0 +1,49 @@
1
+ # Provides 1.1.6 compatibility
2
+ module ActiveRecord
3
+ module Calculations
4
+ module ClassMethods
5
+ protected
6
+ def construct_count_options_from_legacy_args(*args)
7
+ options = {}
8
+ column_name = :all
9
+
10
+ # We need to handle
11
+ # count()
12
+ # count(options={})
13
+ # count(column_name=:all, options={})
14
+ # count(conditions=nil, joins=nil) # deprecated
15
+ if args.size > 2
16
+ raise ArgumentError, "Unexpected parameters passed to count(options={}): #{args.inspect}"
17
+ elsif args.size > 0
18
+ if args[0].is_a?(Hash)
19
+ options = args[0]
20
+ elsif args[1].is_a?(Hash)
21
+ column_name, options = args
22
+ else
23
+ # Deprecated count(conditions, joins=nil)
24
+ ActiveSupport::Deprecation.warn(
25
+ "You called count(#{args[0].inspect}, #{args[1].inspect}), which is a deprecated API call. " +
26
+ "Instead you should use count(column_name, options). Passing the conditions and joins as " +
27
+ "string parameters will be removed in Rails 2.0.", caller(2)
28
+ )
29
+ options.merge!(:conditions => args[0])
30
+ options.merge!(:joins => args[1]) if args[1]
31
+ end
32
+ end
33
+
34
+ [column_name, options]
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ class Module
41
+ def alias_method_chain(target, feature)
42
+ # Strip out punctuation on predicates or bang methods since
43
+ # e.g. target?_without_feature is not a valid method name.
44
+ aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
45
+ yield(aliased_target, punctuation) if block_given?
46
+ alias_method "#{aliased_target}_without_#{feature}#{punctuation}", target
47
+ alias_method target, "#{aliased_target}_with_#{feature}#{punctuation}"
48
+ end
49
+ end
@@ -20,12 +20,14 @@ module ActiveWarehouse
20
20
  dimensions << dimension
21
21
  end
22
22
  end
23
+ alias :pivot_on :pivots_on
23
24
 
24
25
  # Defines the fact that this cube reports on
25
26
  def reports_on(fact)
26
27
  # TODO: Validate if one or more dimension is set
27
28
  @fact = fact
28
29
  end
30
+ alias :report_on :reports_on
29
31
 
30
32
  # Rebuild all aggregate classes. Set :force => true to force the rebuild of aggregate classes.
31
33
  def rebuild(options={})
@@ -163,30 +165,56 @@ module ActiveWarehouse
163
165
  def aggregate_map(column_dimension, column_hierarchy, row_dimension, row_hierarchy, cstage=0, rstage=0)
164
166
  # Fill known cells
165
167
  agg_map = AggregateMap.new
166
- agg_records = aggregate_records(column_dimension, column_hierarchy, row_dimension, row_hierarchy)
167
- agg_records.each do |agg_record|
168
- # agg_record is an instance of Aggregate
169
- # collect the aggregate record data fields into an array
170
- data_array = agg_record.data_fields.collect{ |data_field_name| agg_record.send(data_field_name.to_sym) }
168
+ agg_records = nil
169
+ # s = Benchmark.realtime do
170
+ agg_records = aggregate_records(column_dimension, column_hierarchy, cstage, row_dimension, row_hierarchy, rstage)
171
+ #end
172
+ # cs = 0
173
+ # as = 0
174
+ # calc = 0
175
+ # x = 0
176
+ calculated_fields = self.class.fact_class.calculated_fields
177
+ calculated_field_options = self.class.fact_class.calculated_field_options
178
+ #puts "loading aggregate_records took #{s}s"
179
+ #s = Benchmark.realtime do
180
+ #puts "there are #{agg_records.length} agg_records"
181
+ #puts "there are #{self.class.fact_class.calculated_fields.length} calculated fields in class #{self.class.fact_class}"
182
+ agg_records.each do |agg_record|
183
+ # agg_record is an instance of Aggregate
184
+ # collect the aggregate record data fields into an array
185
+ data_array = nil
186
+ #cs += Benchmark.realtime do
187
+ data_array = agg_record.data_fields.collect{ |data_field_name| agg_record.send(data_field_name.to_sym) }
188
+ #end
171
189
 
172
- # convert to an average where necessary
173
- # TODO: implement
190
+ # convert to an average where necessary
191
+ # TODO: implement
174
192
 
175
- # add calculated fields to the data array
176
- self.class.fact_class.calculated_fields.each do |calculated_field|
177
- options = self.class.fact_class.calculated_field_options[calculated_field]
178
- data_array << options[:block].call(agg_record)
193
+ # add calculated fields to the data array
194
+ #calc += Benchmark.realtime do
195
+ calculated_fields.each do |calculated_field|
196
+ options = calculated_field_options[calculated_field]
197
+ data_array << options[:block].call(agg_record)
198
+ end
199
+ #end
200
+
201
+ # add the data array to the aggregate map
202
+ #as += Benchmark.realtime do
203
+ agg_map.add_data(agg_record.dimension2_path, agg_record.dimension1_path, data_array)
204
+ #end
179
205
  end
180
206
 
181
- # add the data array to the aggregate map
182
- agg_map.add_data(agg_record.dimension2_path, agg_record.dimension1_path, data_array)
183
- end
207
+ #end
208
+ #puts "creating the agg_map took #{s}s"
209
+ #puts "total time spent collecting the data: #{cs}s, avg:#{cs/agg_records.length}s (#{(cs/s) * 100}%)"
210
+ #puts "total time spent adding the data: #{as}s, avg:#{as/agg_records.length}s (#{(as/s) * 100}%)"
211
+ #puts "total time spent calculating fields: #{calc}s, avg:#{calc/agg_records.length}s (#{(calc/s) * 100}%)"
184
212
  agg_map
185
213
  end
186
214
 
187
215
  protected
188
216
  # Return all of the Aggregate records for the specified dimensions and hierarchies
189
- def aggregate_records(column_dimension, column_hierarchy, row_dimension, row_hierarchy)
217
+ def aggregate_records(column_dimension, column_hierarchy, cstage, row_dimension, row_hierarchy, rstage)
190
218
  k = Aggregate.key(column_dimension, column_hierarchy, row_dimension, row_hierarchy)
191
219
  if aggregates[k].nil?
192
220
  self.class.logger.debug("Aggregate #{k} not found in cache")
@@ -217,8 +245,10 @@ module ActiveWarehouse
217
245
  aggregate_class = self.class.aggregates[aggregate_meta_data.id]
218
246
  raise "Cannot find aggregate for id #{aggregate_meta_data.id}" if aggregate_class.nil?
219
247
  end
220
- #puts "Loading aggregate #{aggregate_meta_data.id}"
221
- aggregates[k] = aggregate_class.find(:all)
248
+
249
+ aggregates[k] = aggregate_class.find(:all,
250
+ :conditions => ['(dimension1_stage = ? and dimension2_stage = ?) or (dimension1_stage = ? and dimension2_stage = ?)',
251
+ cstage, rstage, rstage, cstage])
222
252
  end
223
253
  aggregates[k]
224
254
  end
@@ -0,0 +1,296 @@
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
+
5
+ module ActiveWarehouse #:nodoc
6
+ # Dimension tables contain the textual descriptors of the business. Dimensions provide the filters which
7
+ # are applied to facts. Dimensions are the primary source of query constraints, groupings and report
8
+ # labels.
9
+ #
10
+ class Dimension < ActiveRecord::Base
11
+ include ActiveWarehouse::HierarchicalDimension
12
+ include ActiveWarehouse::SlowlyChangingDimension
13
+
14
+ after_save :expire_value_tree_cache
15
+
16
+ class << self
17
+ # Alternate order by, to be used rather than the current level being queried
18
+ attr_accessor :order
19
+
20
+ # Map of level names to alternate order columns
21
+ attr_reader :level_orders
22
+
23
+ # Define a column to order by. If this value is specified then it will be used rather than the actual
24
+ # level being queried in the following method calls:
25
+ # * available_values
26
+ # * available_child_values
27
+ # * available_values_tree
28
+ def set_order(name)
29
+ @order = name
30
+ end
31
+
32
+ # Define a column to order by for a specific level.
33
+ def set_level_order(level, name)
34
+ level_orders[level] = name
35
+ end
36
+
37
+ # Get the level orders map
38
+ def level_orders
39
+ @level_orders ||= {}
40
+ end
41
+
42
+ # Define a named attribute hierarchy in the dimension.
43
+ #
44
+ # Example: define_hierarchy(:fiscal_calendar, [:fiscal_year, :fiscal_quarter, :fiscal_month])
45
+ #
46
+ # This would indicate that one of the drill down paths for this dimension is:
47
+ # Fiscal Year -> Fiscal Quarter -> Fiscal Month
48
+ #
49
+ # Internally the hierarchies are stored in order. The first hierarchy defined will be used as the default
50
+ # if no hierarchy is specified when rendering a cube.
51
+ def define_hierarchy(name, levels)
52
+ hierarchies << name
53
+ hierarchy_levels[name] = levels
54
+ end
55
+
56
+ # Get the named attribute hierarchy. Returns an array of column names.
57
+ #
58
+ # Example: hierarchy(:fiscal_calendar) might return [:fiscal_year, :fiscal_quarter, :fiscal_month]
59
+ def hierarchy(name)
60
+ hierarchy_levels[name]
61
+ end
62
+
63
+ # Get the ordered hierarchy names
64
+ def hierarchies
65
+ @hierarchies ||= []
66
+ end
67
+
68
+ # Get the hierarchy levels hash
69
+ def hierarchy_levels
70
+ @hierarchy_levels ||= {}
71
+ end
72
+
73
+ # Return a symbol used when referring to this dimension. The symbol is calculated by demodulizing and underscoring the
74
+ # dimension's class name and then removing the trailing _dimension.
75
+ #
76
+ # Example: DateDimension will return a symbol :date
77
+ def sym
78
+ self.name.demodulize.underscore.gsub(/_dimension/, '').to_sym
79
+ end
80
+
81
+ # Get the table name. By default the table name will be the name of the dimension in singular form.
82
+ #
83
+ # Example: DateDimension will have a table called date_dimension
84
+ def table_name
85
+ name = self.name.demodulize.underscore
86
+ set_table_name(name)
87
+ name
88
+ end
89
+
90
+ # Convert the given name into a dimension class name
91
+ def class_name(name)
92
+ dimension_name = name.to_s
93
+ dimension_name = "#{dimension_name}_dimension" unless dimension_name =~ /_dimension$/
94
+ dimension_name.classify
95
+ end
96
+
97
+ # Get a class for the specified named dimension
98
+ def class_for_name(name)
99
+ class_name(name).constantize
100
+ end
101
+
102
+ # Return the time when the underlying dimension source file was last modified. This is used
103
+ # to determine if a cube structure rebuild is required
104
+ def last_modified
105
+ File.new(__FILE__).mtime
106
+ end
107
+
108
+ # Get the dimension class for the specified dimension parameter. The dimension parameter may be a class,
109
+ # String or Symbol.
110
+ def to_dimension(dimension)
111
+ return dimension if dimension.is_a?(Class) and dimension.superclass == Dimension
112
+ return class_for_name(dimension)
113
+ end
114
+
115
+ # Returns a hash of all of the values at the specified hierarchy level mapped to the count at that level.
116
+ # For example, given a date dimension with years from 2002 to 2004 and a hierarchy defined with:
117
+ #
118
+ # hierarchy :cy, [:calendar_year, :calendar_quarter, :calendar_month_name]
119
+ #
120
+ # ...then...
121
+ #
122
+ # DateDimension.denominator_count(:cy, :calendar_year, :calendar_quarter) returns {'2002' => 4, '2003' => 4, '2004' => 4}
123
+ #
124
+ # If the denominator_level parameter is omitted or nil then:
125
+ #
126
+ # DateDimension.denominator_count(:cy, :calendar_year) returns {'2003' => 365, '2003' => 365, '2004' => 366}
127
+ #
128
+ def denominator_count(hierarchy_name, level, denominator_level=nil)
129
+ if hierarchy_levels[hierarchy_name].nil?
130
+ raise ArgumentError, "The hierarchy '#{hierarchy_name}' does not exist in your dimension #{name}"
131
+ end
132
+
133
+ q = nil
134
+ # If the denominator_level is specified and it is not the last element in the hierarchy then do a distinct count. If
135
+ # the denominator level is less than the current level then raise an ArgumentError. In other words, if the current level is
136
+ # calendar month then passing in calendar year as the denominator level would raise an ArgumentErro.
137
+ #
138
+ # If the denominator_level is not specified then assume the finest grain possible (in the context of a date dimension
139
+ # this would be each day) and use the id to count.
140
+ if denominator_level && hierarchy_levels[hierarchy_name].last != denominator_level
141
+ level_index = hierarchy_levels[hierarchy_name].index(level)
142
+ denominator_index = hierarchy_levels[hierarchy_name].index(denominator_level)
143
+
144
+ if level_index.nil?
145
+ raise ArgumentError, "The level '#{level}' does not appear to exist"
146
+ end
147
+ if denominator_index.nil?
148
+ raise ArgumentError, "The denominator level '#{denominator_level}' does not appear to exist"
149
+ end
150
+ if hierarchy_levels[hierarchy_name].index(denominator_level) < hierarchy_levels[hierarchy_name].index(level)
151
+ 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}'"
152
+ end
153
+
154
+ q = "select #{level} as level, count(distinct(#{denominator_level})) as level_count from #{table_name} group by #{level}"
155
+ else
156
+ q = "select #{level} as level, count(id) as level_count from #{table_name} group by #{level}"
157
+ end
158
+ denominators = {}
159
+ # TODO: fix to use select_all instead of execute
160
+ connection.select_all(q).each do |row|
161
+ denominators[row['level']] = row['level_count'].to_i
162
+ end
163
+ denominators
164
+ end
165
+
166
+ # Get the foreign key for this dimension which is used in Fact tables.
167
+ #
168
+ # Example: DateDimension would have a foreign key of date_id
169
+ def foreign_key
170
+ table_name.sub(/_dimension/,'') + '_id'
171
+ end
172
+
173
+ # Get an array of the available values for a particular hierarchy level
174
+ # For example, given a DateDimension with data from 2002 to 2004:
175
+ #
176
+ # available_values('calendar_year') returns ['2002','2003','2004']
177
+ def available_values(level)
178
+ level_method = level.to_sym
179
+ level = connection.quote_column_name(level.to_s)
180
+ order = level_orders[level] || self.order || level
181
+ find(:all, :select => level, :group => level, :order => order).collect {|dim| dim.send(level_method)}
182
+ end
183
+
184
+ # Get an array of child values for a particular parent in the hierachy
185
+ # For example, given a DateDimension with data from 2002 to 2004:
186
+ #
187
+ # available_child_values(:cy, [2002, 'Q1']) returns ['January', 'Feburary', 'March', 'April']
188
+ def available_child_values(hierarchy_name, parent_values)
189
+ if hierarchy_levels[hierarchy_name].nil?
190
+ raise ArgumentError, "The hierarchy '#{hierarchy_name}' does not exist in your dimension #{name}"
191
+ end
192
+
193
+ levels = hierarchy_levels[hierarchy_name]
194
+ if levels.length <= parent_values.length
195
+ raise ArgumentError, "The parent_values '#{parent_values.to_yaml}' exceeds the hierarchy depth #{levels.to_yaml}"
196
+ end
197
+
198
+ child_level = levels[parent_values.length].to_s
199
+
200
+ # Create the conditions array. Will work with 1.1.6.
201
+ conditions_parts = []
202
+ conditions_values = []
203
+ parent_values.each_with_index do |value, index|
204
+ conditions_parts << "#{levels[index]} = ?"
205
+ conditions_values << value
206
+ end
207
+ conditions = [conditions_parts.join(' AND ')] + conditions_values unless conditions_parts.empty?
208
+
209
+ child_level_method = child_level.to_sym
210
+ child_level = connection.quote_column_name(child_level)
211
+ order = level_orders[child_level] || self.order || child_level
212
+
213
+ options = {:select => child_level, :group => child_level, :order => order}
214
+ options[:conditions] = conditions unless conditions.nil?
215
+ find(:all, options).collect {|dim| dim.send(child_level_method)}
216
+ end
217
+ alias :available_children_values :available_child_values
218
+
219
+ # Get a tree of Node objects for all of the values in the specified hierarchy.
220
+ def available_values_tree(hierarchy_name)
221
+ root = value_tree_cache[hierarchy_name]
222
+ if root.nil?
223
+ root = Node.new('All', '__ROOT__')
224
+ levels = hierarchy(hierarchy_name)
225
+ nodes = {nil => root}
226
+ level_list = levels.collect{|level| connection.quote_column_name(level) }.join(',')
227
+ order = self.order || level_list
228
+ find(:all, :select => level_list, :group => level_list, :order => order).each do |dim|
229
+ parent_node = root
230
+ levels.each do |level|
231
+ node_value = dim.send(level)
232
+ child_node = parent_node.optionally_add_child(node_value, level)
233
+ parent_node = child_node
234
+ end
235
+ end
236
+ value_tree_cache[hierarchy_name] = root
237
+ end
238
+ root
239
+ end
240
+
241
+ protected
242
+ # Get the value tree cache
243
+ def value_tree_cache
244
+ @value_tree_cache ||= {}
245
+ end
246
+
247
+ class Node#:nodoc:
248
+ attr_reader :value, :children, :parent, :level
249
+
250
+ def initialize(value, level, parent = nil)
251
+ @children = []
252
+ @value = value
253
+ @parent = parent
254
+ @level = level
255
+ end
256
+
257
+ def has_child?(child_value)
258
+ !self.child(child_value).nil?
259
+ end
260
+
261
+ def child(child_value)
262
+ @children.each do |c|
263
+ return c if c.value == child_value
264
+ end
265
+ nil
266
+ end
267
+
268
+ def add_child(child_value, level)
269
+ child = Node.new(child_value, level, self)
270
+ @children << child
271
+ child
272
+ end
273
+
274
+ def optionally_add_child(child_value, level)
275
+ c = child(child_value)
276
+ c = add_child(child_value, level) unless c
277
+ c
278
+ end
279
+ end
280
+
281
+ public
282
+ # Expire the value tree cache. This should be called if the dimension
283
+ def expire_value_tree_cache
284
+ @value_tree_cache = nil
285
+ end
286
+ end
287
+
288
+ public
289
+ def expire_value_tree_cache
290
+ self.class.expire_value_tree_cache
291
+ end
292
+
293
+ end
294
+ end
295
+
296
+ require 'active_warehouse/dimension/bridge'