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
data/README CHANGED
@@ -1,32 +1,58 @@
1
- ActiveWarehouse
2
- ===============
3
- The ActiveWarehouse library provides classes and functions which help with building Data Warehouses using Rails. It can be installed either as a plugin or as a Gem. If you install it as a gem you must include the following in your config/environment.rb:
1
+ == ActiveWarehouse
2
+
3
+ The ActiveWarehouse library provides classes and functions which help with building Data Warehouses using Rails. It can be installed either as a plugin or as a Gem.
4
+
5
+ To install as a plugin just use:
6
+
7
+ script/plugin install --force svn://rubyforge.org/var/svn/activewarehouse/activewarehouse/trunk
8
+
9
+ To get the latest edge version.
10
+
11
+ To install as a Gem, use:
12
+
13
+ gem install activewarehouse
14
+
15
+ At this point you can use ActiveWarehouse in any application using:
4
16
 
5
17
  require_gem 'activewarehouse'
6
18
  require 'active_warehouse'
7
19
 
8
- Additionally, to use the Rake tasks you must include the following in your Rakefile:
20
+ If you want to use the Gem and link to your Rails app then use the gemsonrails project:
21
+
22
+ gem install gemsonrails
23
+
24
+ And then in your Rails app:
9
25
 
10
- load 'tasks/active_warehouse_tasks.rake'
26
+ rake gems:link GEM=activewarehouse
27
+
28
+ == Generators
11
29
 
12
- Generators
13
- ===============
14
30
  ActiveWarehouse comes with several generators
15
31
 
16
- script/generate fact Sales
17
- script/generate fact sales
32
+ script/generate fact Sales
33
+ script/generate fact sales
18
34
 
19
- Creates a SalesFact class and a sales_facts table.
35
+ Creates a SalesFact class and a sales_facts table.
20
36
 
21
- script/generate dimension Region
22
- script/generate dimension region
37
+ script/generate dimension Region
38
+ script/generate dimension region
23
39
 
24
- Creates a RegionDimension class and a region_dimension table.
40
+ Creates a RegionDimension class and a region_dimension table.
25
41
 
26
- script/generate cube RegionalSales
27
- script/generate cube regional_sales
42
+ script/generate cube RegionalSales
43
+ script/generate cube regional_sales
28
44
 
29
- Creates a RegionalSalesCube class.
45
+ Creates a RegionalSalesCube class.
46
+
47
+ script/generate bridge CustomerHierarchy
48
+ script/generate bridge customer_hierarchy
49
+
50
+ Creates a CustomerHierarchyBridge class.
51
+
52
+ script/generate dimension_view OrderDate Date
53
+ script/generate dimension_view order_date date
54
+
55
+ Creates an OrderDateDimension class which is represented by a view on top of the DateDimension.
30
56
 
31
57
  The rules for naming are as follows:
32
58
 
@@ -38,4 +64,23 @@ Dimensions:
38
64
  Both the class name and the table name are suffixed by "_dimension".
39
65
  Cube:
40
66
  Cube class is singular. If a cube table is created it will also be singular.
41
-
67
+ Bridge:
68
+ Bridge classes and tables are both singular.
69
+ Both the class name and the table name are suffixed by "_bridge".
70
+ Dimension View:
71
+ Dimension View classes are singular. The underlying data structure is a view on top of an existing dimension.
72
+ Both the class name and the view name are suffixed by "_dimension"
73
+
74
+ == ETL
75
+
76
+ The ActiveWarehouse plugin does not directly handle Extract-Transform-Load processes, however the ActiveWarehouse ETL gem (installed separately) can help. To install it use:
77
+
78
+ gem install activewarehouse-etl
79
+
80
+ More information on the ETL process can be found at http://activewarehouse.rubyforge.org/etl
81
+
82
+ == Tutorial
83
+
84
+ A tutorial for ActiveWarehouse is available online at http://anthonyeden.com/2006/12/20/activewarehouse-example-with-rails-svn-logs
85
+
86
+ You can also get a demo from the ActiveWarehouse subversion repository. Look in the SVN_ROOT/demo directory.
data/Rakefile CHANGED
@@ -27,6 +27,17 @@ Rake::TestTask.new(:test) do |t|
27
27
  t.verbose = true
28
28
  end
29
29
 
30
+ namespace :test do
31
+ desc 'Measures test coverage'
32
+ task :coverage do
33
+ rm_f "coverage"
34
+ rm_f "coverage.data"
35
+ rcov = "rcov --aggregate coverage.data --text-summary -Ilib"
36
+ system("#{rcov} test/**/*_test.rb")
37
+ system("open coverage/index.html") if PLATFORM['darwin']
38
+ end
39
+ end
40
+
30
41
  desc 'Generate documentation for the active_warehouse plugin.'
31
42
  Rake::RDocTask.new(:rdoc) do |rdoc|
32
43
  rdoc.rdoc_dir = 'rdoc'
@@ -61,6 +72,7 @@ spec = Gem::Specification.new do |s|
61
72
  s.add_dependency('activesupport', '>= 1.3.1.5618')
62
73
  s.add_dependency('activerecord', '>= 1.14.4.5618')
63
74
  s.add_dependency('actionpack', '>= 1.12.5.5618')
75
+ s.add_dependency('rails_sql_views', '>= 0.1.0')
64
76
 
65
77
  s.rdoc_options << '--exclude' << '.'
66
78
  s.has_rdoc = false
@@ -118,4 +130,9 @@ task :release => [ :package ] do
118
130
  puts release_command
119
131
  system(release_command)
120
132
  end
133
+ end
134
+
135
+ desc "Publish the API documentation"
136
+ task :pdoc => [:rdoc] do
137
+ Rake::SshDirPublisher.new("aeden@rubyforge.org", "/var/www/gforge-projects/activewarehouse/rdoc", "rdoc").upload
121
138
  end
@@ -0,0 +1 @@
1
+ ./script/generate bridge NAME
@@ -0,0 +1,46 @@
1
+ class BridgeGenerator < Rails::Generator::NamedBase
2
+ attr_accessor :file_name
3
+
4
+ default_options :skip_migration => false
5
+
6
+ def initialize(runtime_args, runtime_options = {})
7
+ super
8
+
9
+ @name = @name.tableize.singularize
10
+ @table_name = "#{@name}_bridge"
11
+ @class_name = "#{@name.camelize}Bridge"
12
+ @file_name = "#{@class_name.tableize.singularize}"
13
+ end
14
+
15
+ def manifest
16
+ record do |m|
17
+ # Check for class naming collisions.
18
+ m.class_collisions class_path, "#{class_name}", "#{class_name}Test"
19
+
20
+ # Create required directories if necessary
21
+ m.directory File.join('app/models', class_path)
22
+ m.directory File.join('test/unit', class_path)
23
+ m.directory File.join('test/fixtures', class_path)
24
+
25
+ # Generate the files
26
+ m.template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb")
27
+ m.template 'unit_test.rb', File.join('test/unit', class_path, "#{file_name}_test.rb")
28
+ m.template 'fixture.yml', File.join('test/fixtures', class_path, "#{table_name}.yml")
29
+
30
+ # Generate the migration unless :skip_migration option is specified
31
+ unless options[:skip_migration]
32
+ m.migration_template 'migration.rb', 'db/migrate', :assigns => {
33
+ :migration_name => "Create#{class_name.gsub(/::/, '')}"
34
+ }, :migration_file_name => "create_#{file_name.gsub(/\//, '_')}"
35
+ end
36
+ end
37
+ end
38
+
39
+ protected
40
+ def add_options!(opt)
41
+ opt.separator ''
42
+ opt.separator 'Options:'
43
+ opt.on("--skip-migration",
44
+ "Don't generate a migration file for this bridge") { |v| options[:skip_migration] = v }
45
+ end
46
+ end
@@ -0,0 +1,5 @@
1
+ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
2
+ first:
3
+ id: 1
4
+ another:
5
+ id: 2
@@ -0,0 +1,20 @@
1
+ class <%= migration_name %> < ActiveRecord::Migration
2
+ def self.up
3
+ fields = {
4
+ # Add bridge attributes here as name => type
5
+ # Example: :top_flag => :integer
6
+ }
7
+ create_table :<%= table_name %> do |t|
8
+ fields.each do |name,type|
9
+ t.column name, type
10
+ end
11
+ end
12
+ fields.each do |name,type|
13
+ add_index :<%= table_name %>, name unless type == :text
14
+ end
15
+ end
16
+
17
+ def self.down
18
+ drop_table :<%= table_name %>
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ class <%= class_name %> < ActiveWarehouse::Bridge
2
+
3
+ end
@@ -1,8 +1,6 @@
1
1
  require File.dirname(__FILE__) + '<%= '/..' * class_nesting_depth %>/../test_helper'
2
2
 
3
3
  class <%= class_name %>Test < Test::Unit::TestCase
4
- fixtures :<%= table_name %>
5
-
6
4
  # Replace this with your real tests.
7
5
  def test_truth
8
6
  assert true
@@ -0,0 +1 @@
1
+ ./script/generate dimension_view NAME
@@ -0,0 +1,62 @@
1
+ class DimensionViewGenerator < Rails::Generator::NamedBase
2
+ attr_accessor :file_name, :view_name, :query_target_name, :query_target_table_name, :view_query, :view_attributes
3
+
4
+ default_options :skip_migration => false
5
+
6
+ def initialize(runtime_args, runtime_options = {})
7
+ super
8
+
9
+ usage if runtime_args.length != 2
10
+
11
+ @name = @name.tableize.singularize
12
+ @query_target_name = runtime_args[1]
13
+ # define the view and query target table name
14
+ @view_name = "#{@name}_dimension"
15
+ @query_target_table_name = "#{query_target_name}_dimension"
16
+ # define the view class name and query target class name
17
+ @class_name = "#{view_name.camelize}"
18
+ @query_target_class_name = "#{query_target_table_name.camelize}"
19
+ # define the output file name
20
+ @file_name = "#{class_name.tableize.singularize}"
21
+ # define the query target class and expose its columns as attributes for the view
22
+ @query_target_class = @query_target_class_name.constantize
23
+ @view_attributes = @query_target_class.column_names
24
+ @view_query = "select * from #{query_target_table_name}"
25
+ end
26
+
27
+ def manifest
28
+ record do |m|
29
+ # Check for class naming collisions.
30
+ m.class_collisions class_path, "#{class_name}", "#{class_name}Test"
31
+
32
+ # Create required directories if necessary
33
+ m.directory File.join('app/models', class_path)
34
+ m.directory File.join('test/unit', class_path)
35
+ m.directory File.join('test/fixtures', class_path)
36
+
37
+ # Generate the files
38
+ m.template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb")
39
+ m.template 'unit_test.rb', File.join('test/unit', class_path, "#{file_name}_test.rb")
40
+ m.template 'fixture.yml', File.join('test/fixtures', class_path, "#{table_name}.yml")
41
+
42
+ # Generate the migration unless :skip_migration option is specified
43
+ unless options[:skip_migration]
44
+ m.migration_template 'migration.rb', 'db/migrate', :assigns => {
45
+ :migration_name => "Create#{class_name.gsub(/::/, '')}"
46
+ }, :migration_file_name => "create_#{file_name.gsub(/\//, '_')}"
47
+ end
48
+ end
49
+ end
50
+
51
+ def banner
52
+ "Usage: #{$0} #{spec.name} #{spec.name.camelize}Name SelectTarget [options]"
53
+ end
54
+
55
+ protected
56
+ def add_options!(opt)
57
+ opt.separator ''
58
+ opt.separator 'Options:'
59
+ opt.on("--skip-migration",
60
+ "Don't generate a migration file for this dimension view") { |v| options[:skip_migration] = v }
61
+ end
62
+ end
@@ -0,0 +1,11 @@
1
+ class <%= migration_name %> < ActiveRecord::Migration
2
+ def self.up
3
+ create_view "<%= view_name %>", <%= view_query do |t|
4
+ <%= view_attributes %>
5
+ end
6
+ end
7
+
8
+ def self.down
9
+ drop_view "<%= view_name %>"
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ class <%= class_name %> < ActiveWarehouse::DimensionView
2
+
3
+ end
@@ -0,0 +1,10 @@
1
+ require File.dirname(__FILE__) + '<%= '/..' * class_nesting_depth %>/../test_helper'
2
+
3
+ class <%= class_name %>Test < Test::Unit::TestCase
4
+ fixtures :<%= view_name %>
5
+
6
+ # Replace this with your real tests.
7
+ def test_truth
8
+ assert true
9
+ end
10
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'active_warehouse'
@@ -1,5 +1,9 @@
1
+ # This source file requires all of the necessary gems and source files for ActiveWarehouse. If you
2
+ # load this source file all of the other required files and gems will also be brought into the
3
+ # runtime.
4
+
1
5
  #--
2
- # Copyright (c) 2006 Anthony Eden
6
+ # Copyright (c) 2006-2007 Anthony Eden
3
7
  #
4
8
  # Permission is hereby granted, free of charge, to any person obtaining
5
9
  # a copy of this software and associated documentation files (the
@@ -24,13 +28,17 @@
24
28
  $:.unshift(File.dirname(__FILE__)) unless
25
29
  $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
26
30
 
31
+ require 'rubygems'
32
+ unless Kernel.respond_to?(:gem)
33
+ Kernel.send :alias_method, :gem, :require_gem
34
+ end
35
+
27
36
  unless defined?(ActiveSupport)
28
37
  begin
29
38
  $:.unshift(File.dirname(__FILE__) + "/../../activesupport/lib")
30
39
  require 'active_support'
31
40
  rescue LoadError
32
- require 'rubygems'
33
- require_gem 'activesupport'
41
+ gem 'activesupport'
34
42
  end
35
43
  end
36
44
 
@@ -39,8 +47,7 @@ unless defined?(ActiveRecord)
39
47
  $:.unshift(File.dirname(__FILE__) + "/../../activerecord/lib")
40
48
  require 'active_record'
41
49
  rescue LoadError
42
- require 'rubygems'
43
- require_gem 'activerecord'
50
+ gem 'activerecord'
44
51
  end
45
52
  end
46
53
 
@@ -51,15 +58,23 @@ unless defined?(ActionView)
51
58
  require 'action_controller'
52
59
  require 'action_view'
53
60
  rescue LoadError
54
- require 'rubygems'
55
- require_gem 'actionpack'
61
+ gem 'actionpack'
56
62
  end
57
63
  end
58
-
64
+
65
+ # Require 1.1.6 compatibility code if necessary
66
+ require 'active_record/version'
67
+ if ActiveRecord::VERSION::MAJOR < 1 || ActiveRecord::VERSION::MINOR < 15
68
+ require 'active_warehouse/compat/compat'
69
+ end
59
70
 
60
71
  require 'active_warehouse/version'
61
72
  require 'active_warehouse/core_ext'
62
- require 'active_warehouse/model'
73
+ require 'active_warehouse/aggregate'
74
+ require 'active_warehouse/fact'
75
+ require 'active_warehouse/dimension'
76
+ require 'active_warehouse/cube'
77
+ require 'active_warehouse/report'
63
78
  require 'active_warehouse/view'
64
79
  require 'active_warehouse/builder'
65
80
  require 'active_warehouse/migrations'
@@ -1,6 +1,6 @@
1
- module ActiveWarehouse
2
- # An aggreate within a cube used to store calculated values
3
- # Each aggregate will contain values for a dimension pair, down each of the dimension hierarchies
1
+ module ActiveWarehouse #:nodoc:
2
+ # An aggreate within a cube used to store calculated values. Each aggregate will contain values for a dimension pair,
3
+ # down each of the dimension hierarchies
4
4
  class Aggregate < ActiveRecord::Base
5
5
  class << self
6
6
  attr_accessor :name, :cube, :dimension1, :dimension2, :dimension1_hierarchy_name, :dimension2_hierarchy_name
@@ -68,6 +68,8 @@ module ActiveWarehouse
68
68
  #connection.execute("TRUNCATE TABLE #{table_name}") #TODO: make this generic to support all databases
69
69
  delete_all
70
70
 
71
+ $first = false
72
+
71
73
  # aggregate the data for the two dimensions
72
74
  fact_class = cube.fact_class
73
75
  dim1 = Dimension.class_name(dimension1).constantize
@@ -81,29 +83,43 @@ module ActiveWarehouse
81
83
 
82
84
  stmt, fields = build_query(fact_class, dim1, dim1_stage_path, dim2, dim2_stage_path)
83
85
 
86
+ puts "\nSTMT: #{stmt}" if $first
87
+
84
88
  # Get the facts and aggregate them
85
- fact_class.connection.execute(stmt).each do |row|
89
+ # TODO: replace with select_all
90
+ fact_class.connection.select_all(stmt).each do |row|
91
+ require 'pp'
92
+ pp row if $first
86
93
  dim1_value = []
87
- dim1_stage_path.each_with_index do |v, index|
88
- dim1_value << row[index]
94
+ dim1_stage_path.each do |v|
95
+ dim1_value << row["#{v}"]
89
96
  end
90
97
  dim2_value = []
91
- dim2_stage_path.each_with_index do |v, index|
92
- dim2_value << row[dim1_stage_path.length + index]
98
+ dim2_stage_path.each do |v|
99
+ dim2_value << row["#{v}"]
93
100
  end
94
101
 
95
102
  agg_instance = new
96
103
  agg_instance.dimension1_path = dim1_value.join(':')
97
104
  agg_instance.dimension1_stage = dim1_stage_level
98
105
  agg_instance.dimension2_path = dim2_value.join(':')
99
- agg_instance.dimension2_stage = dim2_stage_level
100
- fields.each_with_index do |field, index|
106
+ agg_instance.dimension2_stage = dim2_stage_level
107
+
108
+ puts "DIM1_PATH: #{agg_instance.dimension1_path}" if $first
109
+ puts "DIM2_PATH: #{agg_instance.dimension2_path}" if $first
110
+
111
+
112
+ pp fields if $first
113
+ fields.each do |field|
101
114
  # do the average here
102
- agg_instance.send("#{field}=".to_sym, row[index + dim1_value.length + dim2_value.length])
115
+ puts "setting field #{field}, value is #{row[field.to_s]}" if $first
116
+ agg_instance.send("#{field}=".to_sym, row[field.to_s])
103
117
  end
104
118
  agg_instance.save!
105
119
 
106
120
  meta_data.update_attribute(:populated_at, Time.now)
121
+
122
+ $first = false
107
123
  end
108
124
  end
109
125
  end
@@ -148,9 +164,9 @@ module ActiveWarehouse
148
164
  # put the SQL statement together
149
165
  stmt = "select #{fact_find_options[:select]} from "
150
166
  stmt << "#{fact_class.table_name} f #{fact_find_options[:joins]} "
151
- stmt << "group by #{fact_find_options[:group]}"
167
+ stmt << "group by #{fact_find_options[:group]} "
152
168
 
153
- return stmt, fields
169
+ return stmt.strip, fields
154
170
  end
155
171
 
156
172
  end