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,15 @@
1
+ module ActiveWarehouse
2
+ # Explicit date dimension, because Date dimension has special columns
3
+ # which can be configured to have different names.
4
+ class DateDimension < Dimension
5
+ class << self
6
+ def set_sql_date_stamp(name)
7
+ @sql_date_stamp = name
8
+ end
9
+
10
+ def sql_date_stamp
11
+ @sql_date_stamp ||= "sql_date_stamp"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ module ActiveWarehouse
2
+ module DimensionReflection
3
+ attr_reader :slowly_changing_over
4
+
5
+ def slowly_changing_over=(reflection)
6
+ @slowly_changing_over = reflection
7
+ add_dependent_dimension_reflection(reflection)
8
+ end
9
+
10
+ # add a dependent dimension reflection to this dimension reflection.
11
+ # some dimensions require others to operate, e.g. slowly changing dimensions
12
+ def add_dependent_dimension_reflection(dimension_reflection)
13
+ dependent_dimension_reflections << dimension_reflection
14
+ end
15
+
16
+ # returns array of dependent dimension reflections
17
+ def dependent_dimension_reflections
18
+ @dependent_dimension_reflections ||= []
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ module ActiveWarehouse #:nodoc
2
+ # DimensionViews represent role-playing dimensions in a data warehouse.
3
+ # These types of dimensions provide a view for an existing dimension. A
4
+ # common use is to provide a date dimension and then provide numerous
5
+ # role-playing dimensions implemented as views to the date dimension,
6
+ # such as Order Date Dimension, Shipping Date Dimension, etc.
7
+ class DimensionView < Dimension
8
+ class << self
9
+ def set_order(name)
10
+ super("#{self.sym}_#{name}".to_sym)
11
+ end
12
+ def define_hierarchy(name, hierarchy)
13
+ super(name, hierarchy.collect { |name| "#{self.sym}_#{name}".to_sym })
14
+ end
15
+ end
16
+ def method_missing(method_name, *args)
17
+ unless method_name.to_s =~ /^#{self.class.sym}_/
18
+ method_name = "#{self.class.sym}_#{method_name}".to_sym
19
+ end
20
+ if attribute_present?(method_name)
21
+ read_attribute(method_name)
22
+ else
23
+ raise NameError, "Attribute #{method_name} not found in #{self.class}"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,99 @@
1
+ module ActiveWarehouse #:nodoc
2
+ # Implements a hierarchical dimension. Including the
3
+ # <tt>acts_as_hierarchical_dimension</tt> directive in a dimension will add
4
+ # methods for accessing the parent and children of any node in the hierarchy.
5
+ module HierarchicalDimension
6
+ def self.included(base) #:nodoc
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods #:nodoc
11
+ # Indicates that a dimension is a variable-depth hierarchy.
12
+ def acts_as_hierarchy_dimension
13
+ unless hierarchical_dimension?
14
+ class << self
15
+ # Get the bridge class for this dimension
16
+ def bridge_class
17
+ unless @bridge_class
18
+ unless Object.const_defined?(bridge_class_name.to_sym)
19
+ Object.const_set(bridge_class_name.to_sym, Class.new(ActiveWarehouse::Bridge))
20
+ end
21
+ @bridge_class = Object.const_get(bridge_class_name.to_sym)
22
+ end
23
+ @bridge_class
24
+ end
25
+
26
+ # Get the bridge class name for this hierarchical dimension
27
+ def bridge_class_name
28
+ @child_hierarchy_relationship.class_name || @parent_hierarchy_relationship.class_name
29
+ end
30
+
31
+ # Define the child relationship on the bridge table to the dimension
32
+ # table. We can specify a different class name for the bridge table
33
+ # and different foreign-key.
34
+ def child_bridge(association_id, options = {})
35
+ options[:class_name] ||= name.gsub(/Dimension$/, 'HierarchyBridge')
36
+ options[:foreign_key] ||= "parent_id"
37
+ has_many association_id, options
38
+ @child_hierarchy_relationship = reflections[association_id]
39
+ end
40
+
41
+ # Define the parent relationship on the bridge table to the dimension
42
+ # table.
43
+ def parent_bridge(association_id, options = {})
44
+ options[:class_name] ||= name.gsub(/Dimension$/, 'HierarchyBridge')
45
+ options[:foreign_key] ||= "child_id"
46
+ has_many association_id, options
47
+ @parent_hierarchy_relationship = reflections[association_id]
48
+ end
49
+
50
+ # the foreign key column name on the bridge table for finding the
51
+ # children.
52
+ def child_foreign_key
53
+ @child_hierarchy_relationship.primary_key_name
54
+ end
55
+
56
+ # the foreign key column name on the bridge table for finding the
57
+ # parent.
58
+ def parent_foreign_key
59
+ @parent_hierarchy_relationship.primary_key_name
60
+ end
61
+
62
+ # the column name on the bridge table that defines the number of levels
63
+ # from the parent
64
+ def levels_from_parent
65
+ bridge_class.levels_from_parent
66
+ end
67
+ end
68
+ end
69
+ include InstanceMethods
70
+ end
71
+ alias :acts_as_hierarchical_dimension :acts_as_hierarchy_dimension
72
+
73
+ # Return true if this is a hierarchical dimension
74
+ def hierarchical_dimension?
75
+ self.included_modules.include?(InstanceMethods)
76
+ end
77
+
78
+ end
79
+
80
+ module InstanceMethods #:nodoc
81
+ # Get the parent for this node
82
+ def parent
83
+ self.class.find(:first,
84
+ :select => "a.*",
85
+ :joins => "a join #{self.class.bridge_class.table_name} b on a.id = b.#{self.class.child_foreign_key}",
86
+ :conditions => ["b.#{self.class.parent_foreign_key} = ? and b.#{self.class.levels_from_parent} = 1", self.id])
87
+ end
88
+
89
+ # Get the children for this node
90
+ def children
91
+ self.class.find(:all,
92
+ :select => "a.*",
93
+ :joins => "a join #{self.class.bridge_class.table_name} b on a.id = b.#{self.class.parent_foreign_key}",
94
+ :conditions => ["b.#{self.class.child_foreign_key} = ? and b.#{self.class.levels_from_parent} = 1", self.id])
95
+ end
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,147 @@
1
+ module ActiveWarehouse #:nodoc:
2
+ # Implements Type 2 Slowly Changing Dimensions.
3
+ #
4
+ # In a type 2 SCD, a new row is added each time a dimension entry requires an update. Three columns are required in
5
+ # the dimension table to support this:
6
+ #
7
+ # * latest_version - A boolean flag which indicates whether or not the row is the latest and current value
8
+ # * effective_date - A start date for when this row takes effect
9
+ # * expiration_date - An end date for when this row expires
10
+ #
11
+ # This module will override finder behavior in several ways. If used in a normal fashion, the find method will return
12
+ # the match row or rows with the latest_version flag set to true. You can also call the finder with the :valid_on
13
+ # option set indicating that you want the row that is valid on the given date.
14
+ #
15
+ # You can completely override the modified finder behavior using the :with_older option (set to true). You *must* include
16
+ # this option if you want to search for records which are not current (for example, using the find(id) version of the finder
17
+ # methods).
18
+ module SlowlyChangingDimension
19
+ def self.included(base) # :nodoc:
20
+ base.extend ClassMethods
21
+ end
22
+
23
+ module ClassMethods
24
+ # Indicate that the dimension is a Type 2 Slowly Changing Dimension (SDC).
25
+ #
26
+ # A word of warning:
27
+ #
28
+ # The expiration_date field must never be null. For the current effective record use the maximum date allowed.
29
+ # This is necessary because the find query used when :valid_on is specified is implemented using a between clause.
30
+ #
31
+ # Options:
32
+ # * <tt>:identifier</tt>:
33
+ # * <tt>:lastest_version</tt>: Define the attribute name which represents the latest version flag
34
+ # * <tt>:effective_date</tt>: Define the attribute name which represents the effective date column
35
+ # * <tt>:expiration_date</tt>: Define the attribute name which represents the expiration date column
36
+ #
37
+ def acts_as_slowly_changing_dimension(options = {})
38
+ unless slowly_changing_dimension? # don't let AR call this twice
39
+ cattr_accessor :identifier
40
+ cattr_accessor :latest_version_attribute
41
+ cattr_accessor :effective_date_attribute
42
+ cattr_accessor :expiration_date_attribute
43
+ self.identifier = options[:identifier] || :identifier
44
+ self.latest_version_attribute = options[:with] || :latest_version
45
+ self.effective_date_attribute = options[:effective_date_attribute] || :effective_date
46
+ self.expiration_date_attribute = options[:expiration_date_attribute] || :expiration_date
47
+ class << self
48
+ alias_method :find_every_with_older, :find_every
49
+ alias_method :calculate_with_older, :calculate
50
+ alias_method :core_validate_find_options, :validate_find_options
51
+ VALID_FIND_OPTIONS << :with_older
52
+ VALID_FIND_OPTIONS << :valid_on
53
+ VALID_FIND_OPTIONS << :valid_during
54
+ end
55
+ end
56
+ include InstanceMethods
57
+ end
58
+
59
+ # Return true if this dimension is a slowly changing dimension
60
+ def slowly_changing_dimension?
61
+ self.included_modules.include?(InstanceMethods)
62
+ end
63
+ end
64
+
65
+ module InstanceMethods #:nodoc:
66
+ def self.included(base) # :nodoc:
67
+ base.extend ClassMethods
68
+ end
69
+
70
+ def versions
71
+ self.class.find(:all,
72
+ :conditions => ["#{self.class.identifier} = ?", self.send(identifier)],
73
+ :with_older => true,
74
+ :order => "#{self.class.effective_date_attribute} asc")
75
+ end
76
+
77
+ module ClassMethods
78
+ def find_with_older(*args)
79
+ options = extract_options_from_args!(args)
80
+ validate_find_options(options)
81
+ set_readonly_option!(options)
82
+ options[:with_older] = true # yuck!
83
+
84
+ case args.first
85
+ when :first then find_initial(options)
86
+ when :all then find_every(options)
87
+ else find_from_ids(args, options)
88
+ end
89
+ end
90
+
91
+ def count_with_older(*args)
92
+ calculate_with_older(:count, *construct_count_options_from_legacy_args(*args))
93
+ end
94
+
95
+ def count(*args)
96
+ with_older_scope { count_with_older(*args) }
97
+ end
98
+
99
+ def calculate(*args)
100
+ with_older_scope { calculate_with_older(*args) }
101
+ end
102
+
103
+ protected
104
+ def with_older_scope(&block)
105
+ with_scope({:find => { :conditions =>
106
+ ["#{table_name}.#{latest_version_attribute} = ?", true] } }, :merge, &block)
107
+ end
108
+
109
+ def with_valid_on_scope(valid_on, &block)
110
+ with_scope({:find => { :conditions =>
111
+ ["? between #{effective_date_attribute} " +
112
+ "and #{expiration_date_attribute}", valid_on]} }, :merge, &block)
113
+ end
114
+
115
+ def with_valid_during_scope(valid_during, &block)
116
+ with_scope({:find => {:conditions =>
117
+ ["(? between #{effective_date_attribute} and #{expiration_date_attribute})" +
118
+ " or (#{effective_date_attribute} between ? and ?)",
119
+ valid_during.first, valid_during.first, valid_during.last]} }, :merge, &block)
120
+ end
121
+
122
+ private
123
+ # all find calls lead here
124
+ def find_every(options)
125
+ if options.include?(:valid_on)
126
+ with_valid_on_scope(options[:valid_on]) { find_every_with_older(options) }
127
+ elsif options.include?(:valid_during)
128
+ if !options.include?(:order)
129
+ options[:order] = "#{effective_date_attribute} asc"
130
+ end
131
+ if !options.include?(:limit)
132
+ options[:limit] = 1
133
+ end
134
+ if !options.include?(:offset)
135
+ options[:offset] = 0
136
+ end
137
+ with_valid_during_scope(options[:valid_during]) { find_every_with_older(options) }
138
+ elsif options.include?(:with_older)
139
+ find_every_with_older(options)
140
+ else
141
+ with_older_scope { find_every_with_older(options) }
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,239 @@
1
+ module ActiveWarehouse #:nodoc
2
+
3
+ # Facts represent business measures. A row in a fact table corresponds to set
4
+ # of measurements in a particular
5
+ # granularity along with the foreign keys connecting the fact to various
6
+ # dimensions. All measurements in a fact
7
+ # table must be at the same grain.
8
+ class Fact < ActiveRecord::Base
9
+ class << self
10
+ # Array of AggregateField instances
11
+ attr_accessor :aggregate_fields
12
+
13
+ # Array of calculated field names
14
+ attr_accessor :calculated_fields
15
+
16
+ # Hash of calculated field options, where the key is the field name
17
+ # and the value is the Hash of options for that calculated field.
18
+ attr_accessor :calculated_field_options
19
+
20
+ # Array of belongs_to +Reflection+ instances that represent the
21
+ # dimensions for this fact.
22
+ attr_accessor :dimension_relationships
23
+
24
+ # Acts as an alias for +belongs_to+, yet marks this relationship
25
+ # as a dimension. You must call +dimension+ instead of +belongs_to+.
26
+ # Accepts same options as +belongs_to+.
27
+ def dimension(association_id, options = {})
28
+ options[:class_name] ||= "#{association_id}Dimension".classify
29
+ options[:foreign_key] ||= "#{association_id}_id"
30
+ slowly_changing_over = options.delete(:slowly_changing)
31
+ belongs_to association_id, options
32
+ relationship = reflections[association_id]
33
+
34
+ if slowly_changing_over
35
+ if !dimensions.include?(slowly_changing_over)
36
+ raise "No dimension specified with name '#{slowly_changing_over}' in fact '#{self.name}', specify it first with dimension macro"
37
+ end
38
+ relationship.slowly_changing_over = dimension_relationships[slowly_changing_over]
39
+ end
40
+
41
+ dimension_relationships[association_id] = relationship
42
+ end
43
+
44
+ # Acts as an alias for +has_and_belongs_to_many+, yet marks this relationship
45
+ # as a dimension. You must call +has_and_belongs_to_many_dimension+
46
+ # instead of +has_and_belongs_to_many+.
47
+ # Accepts same options as +has_and_belongs_to_many+.
48
+ def has_and_belongs_to_many_dimension(association_id, options = {})
49
+ options[:class_name] ||= "#{association_id}Dimension".classify
50
+ options[:association_foreign_key] ||= "#{association_id}_id"
51
+ name = self.name.demodulize.chomp('Fact').underscore
52
+ options[:join_table] ||= "#{name}_#{association_id}_bridge"
53
+ has_and_belongs_to_many association_id, options
54
+ relationship = reflections[association_id]
55
+ dimension_relationships[association_id] = relationship
56
+ end
57
+
58
+ # returns true for the dimension relationship of +belongs_to+
59
+ def belongs_to_relationship?(dimension_name)
60
+ dimension_relationships[dimension_name] and dimension_relationships[dimension_name].macro == :belongs_to
61
+ end
62
+
63
+ # returns true for the dimension relationship of +has_and_belongs_to_many+
64
+ def has_and_belongs_to_many_relationship?(dimension_name)
65
+ dimension_relationships[dimension_name] and dimension_relationships[dimension_name].macro == :has_and_belongs_to_many
66
+ end
67
+
68
+ # returns the AssociationReflection for the specified dimension name
69
+ def dimension_relationship(dimension_name)
70
+ dimension_relationships[dimension_name]
71
+ end
72
+
73
+ # returns the dimension name (as specified in the dimension macro)
74
+ # which the specified +dimension_name+ is slowly changing over
75
+ def slowly_changes_over_name(dimension_name)
76
+ dimension_relationships[dimension_name].slowly_changing_over.name
77
+ end
78
+
79
+ # returns the Class for the dimension which the specified
80
+ # +dimension_name+ is slowly changing over
81
+ def slowly_changes_over_class(dimension_name)
82
+ dimension_class(slowly_changes_over_name(dimension_name))
83
+ end
84
+
85
+ # Return a list of dimensions for this fact.
86
+ #
87
+ # Example:
88
+ #
89
+ # sales_fact
90
+ # date_id
91
+ # region_id
92
+ # sales_amount
93
+ # number_items_sold
94
+ #
95
+ # Calling SalesFact.dimensions would return the list: [:date, :region]
96
+ def dimensions
97
+ dimension_relationships.collect { |k,v| k }
98
+ end
99
+
100
+ # Returns the dimension class, given a dimension name from this fact.
101
+ # Must appear as a registered dimension relationship.
102
+ def dimension_class(dimension_name)
103
+ dimension_relationships[dimension_name.to_sym].class_name.constantize
104
+ end
105
+
106
+ # Get the time when the fact source file was last modified
107
+ def last_modified
108
+ File.new(__FILE__).mtime
109
+ end
110
+
111
+ # Get the table name. The fact table name is pluralized
112
+ def table_name
113
+ name = self.name.demodulize.underscore.pluralize
114
+ set_table_name(name)
115
+ name
116
+ end
117
+
118
+ # Get the class name for the specified fact name
119
+ def class_name(name)
120
+ fact_name = name.to_s
121
+ fact_name = "#{fact_name}_facts" unless fact_name =~ /_fact[s?]$/
122
+ fact_name.classify
123
+ end
124
+
125
+ # Get the class for the specified fact name
126
+ def class_for_name(name)
127
+ class_name(name).constantize
128
+ end
129
+
130
+ # Get the fact class for the specified value. The fact parameter may be a class,
131
+ # String or Symbol.
132
+ def to_fact(fact_name)
133
+ return fact_name if fact_name.is_a?(Class) and fact_name.superclass == Fact
134
+ return class_for_name(fact_name)
135
+ end
136
+
137
+ # Return the foreign key that the fact uses to relate back to the specified
138
+ # dimension. This is found using the dimension_relationships hash.
139
+ def foreign_key_for(dimension_name)
140
+ dimension_relationships[dimension_name].primary_key_name
141
+ end
142
+
143
+ # Define an aggregate. Also aliased from aggregate()
144
+ # * <tt>field</tt>: The field name
145
+ # * <tt>options</tt>: A hash of options for the aggregate
146
+ def define_aggregate(field, options={})
147
+ if columns_hash[field.to_s].nil?
148
+ raise ArgumentError, "Field #{field} does not exist in table #{table_name}"
149
+ end
150
+ options[:type] ||= :sum
151
+
152
+ aggregate_field = AggregateField.new(self, columns_hash[field.to_s],
153
+ options[:type], options)
154
+ aggregate_fields << aggregate_field
155
+ end
156
+ alias :aggregate :define_aggregate
157
+
158
+ # Define prejoined fields from a dimension of the fact. Also aliased
159
+ # from prejoin()
160
+ # * <tt>field</tt>: A hash with the key of dimension and an array
161
+ # of attributes from the dimension as value
162
+ def define_prejoin(field)
163
+ prejoined_fields.merge!(field)
164
+ end
165
+ alias :prejoin :define_prejoin
166
+
167
+ # Define a calculated field
168
+ # * <tt>field</tt>: The field name
169
+ # * <tt>options</tt>: An options hash
170
+ #
171
+ # This method takes a block which will be passed the current aggregate record.
172
+ #
173
+ # Example: calculated_field (:gross_margin) { |r| r.gross_profit_dollar_amount / r.sales_dollar_amount}
174
+ def calculated_field(field, options={}, &block)
175
+ calculated_fields << CalculatedField.new(self, field, options[:type], options, &block)
176
+ end
177
+
178
+ # Returns true if this fact has at least one fact that is semiadditive,
179
+ # or false
180
+ def has_semiadditive_fact?
181
+ aggregate_fields.each do |field|
182
+ return true if field.is_semiadditive?
183
+ end
184
+ return false
185
+ end
186
+
187
+ # Get a list of all calculated fields
188
+ def calculated_fields
189
+ @calculated_field ||= []
190
+ end
191
+
192
+ # Get the CalculatedField instance for the specified name
193
+ def calculated_field_for_name(name)
194
+ calculated_fields.find {|f| f.name.to_s == name.to_s}
195
+ end
196
+
197
+ # Get a list of all aggregate fields
198
+ def aggregate_fields
199
+ @aggregate_fields ||= []
200
+ end
201
+
202
+ # Get the AggregateField instance for the specified name.
203
+ def aggregate_field_for_name(name)
204
+ aggregate_fields.find {|f| f.name.to_s == name.to_s}
205
+ end
206
+
207
+ # Get the field instance for the specified name. Looks in aggregate fields first, then
208
+ # calculated fields
209
+ def field_for_name(name)
210
+ field = aggregate_fields.find {|f| f.name.to_s == name.to_s}
211
+ field = calculated_fields.find {|f| f.name.to_s == name.to_s} unless field
212
+ field
213
+ end
214
+
215
+ # The table name to use for the prejoined fact table
216
+ def prejoined_table_name
217
+ "prejoined_#{table_name}"
218
+ end
219
+
220
+ # Get the hash of all prejoined fields
221
+ def prejoined_fields
222
+ @prejoined_fields ||= {}
223
+ end
224
+
225
+ def dimension_relationships
226
+ @dimension_relationships ||= OrderedHash.new
227
+ end
228
+
229
+ def prejoin_fact
230
+ @prejoin_fact ||= ActiveWarehouse::PrejoinFact.new(self)
231
+ end
232
+
233
+ def populate
234
+ prejoin_fact.populate
235
+ end
236
+
237
+ end
238
+ end
239
+ end