activewarehouse 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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'