blackbird 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.gitignore +8 -0
  2. data/CHANGELOG.md +9 -0
  3. data/Gemfile +6 -0
  4. data/LICENSE +19 -0
  5. data/README.md +77 -0
  6. data/ROADMAP.md +5 -0
  7. data/Rakefile +15 -0
  8. data/blackbird.gemspec +22 -0
  9. data/lib/blackbird.rb +39 -0
  10. data/lib/blackbird/column.rb +32 -0
  11. data/lib/blackbird/fragment.rb +58 -0
  12. data/lib/blackbird/helpers/join_tables.rb +50 -0
  13. data/lib/blackbird/helpers/typed_columns.rb +50 -0
  14. data/lib/blackbird/index.rb +29 -0
  15. data/lib/blackbird/migration.rb +218 -0
  16. data/lib/blackbird/patch.rb +64 -0
  17. data/lib/blackbird/processor_list.rb +41 -0
  18. data/lib/blackbird/processors/indexed_columns.rb +22 -0
  19. data/lib/blackbird/processors/normal_default.rb +7 -0
  20. data/lib/blackbird/railtie.rb +55 -0
  21. data/lib/blackbird/railtie/tasks.rake +8 -0
  22. data/lib/blackbird/schema.rb +25 -0
  23. data/lib/blackbird/schema/builder.rb +31 -0
  24. data/lib/blackbird/schema/changes.rb +77 -0
  25. data/lib/blackbird/schema/loader.rb +79 -0
  26. data/lib/blackbird/table.rb +71 -0
  27. data/lib/blackbird/table/builder.rb +124 -0
  28. data/lib/blackbird/table/changes.rb +86 -0
  29. data/lib/blackbird/transition.rb +85 -0
  30. data/lib/blackbird/version.rb +3 -0
  31. data/lib/rails/generators/active_record/transition/templates/fragment.rb +12 -0
  32. data/lib/rails/generators/active_record/transition/transition_generator.rb +25 -0
  33. data/spec/fixtures/a/comments_fragment.rb +10 -0
  34. data/spec/fixtures/a/pages_fragment.rb +9 -0
  35. data/spec/fixtures/a/posts_fragment.rb +9 -0
  36. data/spec/fixtures/a/users_fragment.rb +7 -0
  37. data/spec/fixtures/b/comments_fragment.rb +23 -0
  38. data/spec/fixtures/b/posts_fragment.rb +8 -0
  39. data/spec/spec.opts +4 -0
  40. data/spec/spec_helper.rb +72 -0
  41. data/spec/transitions/migration_spec.rb +116 -0
  42. data/spec/transitions/schema_loader_spec.rb +35 -0
  43. metadata +127 -0
@@ -0,0 +1,124 @@
1
+ class Blackbird::Table::Builder
2
+
3
+ def initialize(schema, fragment, table)
4
+ @schema, @fragment, @table = schema, fragment, table
5
+ end
6
+
7
+ ##
8
+ # Scope a number of column definitions. All the column and index
9
+ # options can be passed here. also :prefix and :suffix can be passed
10
+ # and they prefix or suffix the names of the indexes and columns.
11
+ def scope(options={}, &block)
12
+ _scope, @scope = @scope, (@scope || {}).merge(options)
13
+ if block.arity != 1
14
+ instance_eval(&block)
15
+ else
16
+ yield(self)
17
+ end
18
+ ensure
19
+ @scope = _scope
20
+ end
21
+
22
+ ##
23
+ # Remove any previously defined scopes
24
+ def without_scope(&block)
25
+ _scope, @scope = @scope, nil
26
+ if block.arity != 1
27
+ instance_eval(&block)
28
+ else
29
+ yield(self)
30
+ end
31
+ ensure
32
+ @scope = _scope
33
+ end
34
+
35
+
36
+ ##
37
+ # Define a column
38
+ def column(name, type, options={})
39
+ options = @scope.merge(options) if @scope
40
+ options = { :null => true }.merge(options)
41
+
42
+ name = "#{options[:prefix]}#{name}" if options[:prefix]
43
+ name = "#{name}#{options[:suffix]}" if options[:suffix]
44
+
45
+ if @table.columns[name.to_s]
46
+ @table.change_column(name, type, options)
47
+ else
48
+ @table.add_column(name, type, options)
49
+ end
50
+ end
51
+
52
+ ##
53
+ # Remove a column
54
+ def remove_column(name)
55
+ options = @scope || {}
56
+
57
+ name = "#{options[:prefix]}#{name}" if options[:prefix]
58
+ name = "#{name}#{options[:suffix]}" if options[:suffix]
59
+
60
+ @table.remove_column(name)
61
+ end
62
+
63
+
64
+ ##
65
+ # Define an index
66
+ def index(columns, options={})
67
+ options = @scope.merge(options) if @scope
68
+
69
+ columns = [columns].flatten.collect do |name|
70
+ name = "#{options[:prefix]}#{name}" if options[:prefix]
71
+ name = "#{name}#{options[:suffix]}" if options[:suffix]
72
+ name
73
+ end
74
+
75
+ @table.add_index(columns, options)
76
+ end
77
+
78
+ ##
79
+ # Remove an index
80
+ def remove_index(name)
81
+ options = @scope || {}
82
+
83
+ name = "#{options[:prefix]}#{name}" if options[:prefix]
84
+ name = "#{name}#{options[:suffix]}" if options[:suffix]
85
+
86
+ @table.remove_index(name)
87
+ end
88
+
89
+ ##
90
+ # Rename a previously defined columns. This also creates a patch to
91
+ # migrate any data in the old column to the new column.
92
+ def rename(old_name, new_name)
93
+ options = @scope
94
+ if options
95
+ new_name = "#{options[:prefix]}#{new_name}" if options[:prefix]
96
+ new_name = "#{new_name}#{options[:suffix]}" if options[:suffix]
97
+
98
+ old_name = "#{options[:prefix]}#{old_name}" if options[:prefix]
99
+ old_name = "#{old_name}#{options[:suffix]}" if options[:suffix]
100
+ end
101
+
102
+ column = @table.columns[old_name.to_s]
103
+
104
+ without_scope do
105
+ self.remove old_name
106
+ self.column new_name, column.type, column.options
107
+ end
108
+
109
+ msg = "Renaming #{@table.name}.#{old_name} to #{@table.name}.#{new_name}"
110
+ patch = Blackbird::Patch.new(@fragment, msg) do |p|
111
+ t = p.table(@table.name)
112
+
113
+ if t.add?(new_name) and t.remove?(old_name)
114
+ t.rename(old_name, new_name)
115
+ end
116
+ end
117
+ @schema.patches[patch.name] = patch
118
+ end
119
+
120
+ def remove(name)
121
+ remove_column name
122
+ end
123
+
124
+ end
@@ -0,0 +1,86 @@
1
+ class Blackbird::Table::Changes
2
+
3
+ def self.analyze!(current, future)
4
+ new(current, future).analyze!
5
+ end
6
+
7
+ attr_reader :current, :future
8
+ attr_reader :new_columns, :old_columns, :all_columns, :changed_columns, :unchanged_columns, :renamed_columns
9
+ attr_reader :new_indexes, :old_indexes, :all_indexes, :changed_indexes, :unchanged_indexes
10
+
11
+ def initialize(current, future)
12
+ @current, @future = current, future
13
+ @renamed_columns = []
14
+ end
15
+
16
+ def analyze!
17
+ current_columns = @current ? @current.columns : {}
18
+ future_columns = @future ? @future.columns : {}
19
+
20
+ @new_columns = (future_columns.keys - current_columns.keys)
21
+ @old_columns = (current_columns.keys - future_columns.keys)
22
+ @all_columns = (current_columns.keys | future_columns.keys)
23
+ @changed_columns = []
24
+ (current_columns.keys & future_columns.keys).each do |name|
25
+ if current_columns[name] != future_columns[name]
26
+ @changed_columns << name
27
+ end
28
+ end
29
+ @unchanged_columns = (current_columns.keys & future_columns.keys) - @changed_columns
30
+
31
+ current_indexes = @current ? @current.indexes : {}
32
+ future_indexes = @future ? @future.indexes : {}
33
+
34
+ @new_indexes = (future_indexes.keys - current_indexes.keys)
35
+ @old_indexes = (current_indexes.keys - future_indexes.keys)
36
+ @all_indexes = (current_indexes.keys | future_indexes.keys)
37
+ @changed_indexes = []
38
+ (current_indexes.keys & future_indexes.keys).each do |name|
39
+ if current_indexes[name] != future_indexes[name]
40
+ p [name, current_indexes[name], future_indexes[name]]
41
+ @changed_indexes << name
42
+ end
43
+ end
44
+ @unchanged_indexes = (current_indexes.keys & future_indexes.keys) - @changed_indexes
45
+
46
+ self
47
+ end
48
+
49
+ def has_changes?
50
+ !((@unchanged_columns == @all_columns) and
51
+ (@unchanged_indexes == @all_indexes))
52
+ end
53
+
54
+ def add?(name)
55
+ @new_columns.include?(name.to_s)
56
+ end
57
+
58
+ def add_index?(name)
59
+ @new_indexes.include?(name.to_s)
60
+ end
61
+
62
+ def remove?(name)
63
+ @old_columns.include?(name.to_s)
64
+ end
65
+
66
+ def remove_index?(name)
67
+ @old_indexes.include?(name.to_s)
68
+ end
69
+
70
+ def change?(name)
71
+ @changed_columns.include?(name.to_s)
72
+ end
73
+
74
+ def change_index?(name)
75
+ @changed_indexes.include?(name.to_s)
76
+ end
77
+
78
+ def exists?(name)
79
+ @all_columns.include?(name.to_s)
80
+ end
81
+
82
+ def index_exists?(name)
83
+ @all_indexes.include?(name.to_s)
84
+ end
85
+
86
+ end
@@ -0,0 +1,85 @@
1
+ # Blackbird::Transition is the coordinator of the transition process
2
+ class Blackbird::Transition
3
+
4
+ def self.run!(*fragment_files)
5
+ build(fragment_files).run!
6
+ end
7
+
8
+ def self.build(*fragment_files)
9
+ new(fragment_files).build
10
+ end
11
+
12
+ attr_reader :fragment_files, :current, :future, :changes, :migration, :fragments
13
+
14
+ def initialize(*fragment_files)
15
+ @fragment_files = fragment_files.flatten.uniq.compact
16
+ end
17
+
18
+ def build
19
+ load_fragments
20
+ load_current
21
+ build_future
22
+ analyze_changes
23
+ build_migration
24
+
25
+ self
26
+ end
27
+
28
+ def run!
29
+ connection = ActiveRecord::Base.connection
30
+ connection.transaction do
31
+
32
+ @migration.instructions.each do |instruction|
33
+ case instruction.first
34
+ when :apply
35
+ instruction.last.call
36
+ when :log
37
+ puts instruction.last if Blackbird.options[:verbose]
38
+ when :create_table
39
+ connection.__send__(*instruction) {}
40
+ else
41
+ connection.__send__(*instruction)
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ self
48
+ end
49
+
50
+ private
51
+
52
+ def load_fragments
53
+ @fragment_files.each do |path|
54
+ require path
55
+ end
56
+ end
57
+
58
+ def load_current
59
+ @current = Blackbird::Schema::Loader.load
60
+ end
61
+
62
+ def build_future
63
+ @future = Blackbird::Schema.new
64
+ @fragments = ActiveSupport::OrderedHash.new
65
+ builder = Blackbird::Schema::Builder.new(@future)
66
+ Blackbird::Fragment.subclasses.each do |fragment|
67
+ fragment = fragment.new
68
+ @fragments[fragment.class] = fragment
69
+ fragment.apply(builder)
70
+ end
71
+
72
+ Blackbird.options[:processors].build.each do |processor|
73
+ @future.process processor
74
+ end
75
+ end
76
+
77
+ def analyze_changes
78
+ @changes = Blackbird::Schema::Changes.analyze!(@current, @future)
79
+ end
80
+
81
+ def build_migration
82
+ @migration = Blackbird::Migration.build(@current, @future, @changes)
83
+ end
84
+
85
+ end
@@ -0,0 +1,3 @@
1
+ module Blackbird
2
+ VERSION = '1.0.0.pre'
3
+ end
@@ -0,0 +1,12 @@
1
+ class <%= class_name.pluralize %>Fragment < Blackbird::Fragment
2
+
3
+ table(:<%= class_name.gsub('::', '').underscore.pluralize %>) do |t|
4
+ <% for attribute in attributes -%>
5
+ t.<%= attribute.type %> :<%= attribute.name %>
6
+ <% end -%>
7
+ <% if options[:timestamps] %>
8
+ t.timestamps
9
+ <% end -%>
10
+ end
11
+
12
+ end
@@ -0,0 +1,25 @@
1
+ require 'rails/generators/rails/model/model_generator'
2
+
3
+ Rails::Generators::ModelGenerator.hook_for :transition,
4
+ :in => :active_record,
5
+ :default => true, :type => :boolean
6
+
7
+ module ActiveRecord
8
+ module Generators
9
+ class TransitionGenerator < Rails::Generators::NamedBase
10
+ argument :attributes, :type => :array, :default => [], :banner => "field:type field:type"
11
+
12
+ def self.source_root
13
+ @_tr_source_root ||= begin
14
+ if base_name && generator_name
15
+ File.expand_path('../templates', __FILE__)
16
+ end
17
+ end
18
+ end
19
+
20
+ def create_schema_file
21
+ template 'fragment.rb', File.join('app/schema', class_path, "#{file_name.pluralize}_fragment.rb")
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,10 @@
1
+ class CommentsFragment < Blackbird::Fragment
2
+
3
+ table :comments do |t|
4
+ t.integer :post_id, :index => true
5
+ t.integer :user_id, :index => true
6
+ t.text :body
7
+ t.datetime :published_at, :index => true
8
+ end
9
+
10
+ end
@@ -0,0 +1,9 @@
1
+ class PagesFragment < Blackbird::Fragment
2
+
3
+ table :pages do |t|
4
+ t.string :title, :index => true
5
+ t.text :body
6
+ t.datetime :published_at, :index => true
7
+ end
8
+
9
+ end
@@ -0,0 +1,9 @@
1
+ class PostsFragment < Blackbird::Fragment
2
+
3
+ table :posts do |t|
4
+ t.string :title, :unique => true
5
+ t.text :body
6
+ t.datetime :published_at, :index => true
7
+ end
8
+
9
+ end
@@ -0,0 +1,7 @@
1
+ class UsersFragment < Blackbird::Fragment
2
+
3
+ table :users do |t|
4
+ t.string :full_name
5
+ end
6
+
7
+ end
@@ -0,0 +1,23 @@
1
+ class CommentsFragment < Blackbird::Fragment
2
+
3
+ table :comments do |t|
4
+ t.rename :published_at, :posted_at
5
+ t.string :username
6
+ end
7
+
8
+ patch "Add usernames based on user_ids" do |p|
9
+ t = p.table(:comments)
10
+
11
+ if t.add?(:username) and t.exists?(:user_id)
12
+ t.apply :set_user_names
13
+ end
14
+ end
15
+
16
+ def set_user_names
17
+ user_names = select_rows %{ SELECT comments.id, users.full_name FROM comments LEFT JOIN users ON users.id = comments.user_id }
18
+ user_names.each do |(id, name)|
19
+ execute %{ UPDATE comments SET username = ? WHERE id = ? }, name, id
20
+ end
21
+ end
22
+
23
+ end
@@ -0,0 +1,8 @@
1
+ class PostsFragment < Blackbird::Fragment
2
+
3
+ table :posts do |t|
4
+ t.string :title, :unique => true
5
+ t.rename :published_at, :posted_at
6
+ end
7
+
8
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,4 @@
1
+ --require spec/spec_helper.rb
2
+ --format specdoc
3
+ --colour
4
+ --diff
@@ -0,0 +1,72 @@
1
+ require 'rubygems'
2
+
3
+ case ENV['RAILS_VERSION']
4
+ when '3.0', nil
5
+ gem 'activerecord', '~> 3.0.0'
6
+ when '2.3'
7
+ gem 'activerecord', '~> 2.3.0'
8
+ else
9
+ gem 'activerecord', ENV['RAILS_VERSION']
10
+ end
11
+
12
+ require 'blackbird'
13
+ require 'fileutils'
14
+ require 'pp'
15
+
16
+ puts "Running with ActiveRecord version #{ActiveRecord::VERSION::STRING}"
17
+
18
+ tmp = File.expand_path('../../tmp', __FILE__)
19
+ FileUtils.mkdir_p(tmp)
20
+ FileUtils.rm_f(tmp)
21
+
22
+ ActiveRecord::Base.establish_connection({
23
+ :adapter => 'sqlite3',
24
+ :database => tmp+'/test.db'
25
+ })
26
+
27
+ ActiveRecord::Schema.define do
28
+ create_table "posts", :force => true do |t|
29
+ t.column "title", :string
30
+ t.column "body", :text
31
+ t.column "published_at", :datetime
32
+ end
33
+
34
+ create_table "pages", :force => true do |t|
35
+ t.column "title", :string
36
+ t.column "body", :string
37
+ t.column "image_id", :integer
38
+ end
39
+
40
+ create_table "images", :force => true do |t|
41
+ t.column "caption", :text
42
+ t.column "filename", :string
43
+ end
44
+
45
+ create_table "users", :force => true do |t|
46
+ t.column "full_name", :string
47
+ end
48
+
49
+ ActiveRecord::Base.connection.execute('INSERT INTO users (id, full_name) VALUES (1, "Simon Menke")')
50
+ ActiveRecord::Base.connection.execute('INSERT INTO users (id, full_name) VALUES (2, "Yves")')
51
+ end
52
+
53
+
54
+ def reset_connection
55
+ tmp = File.expand_path('../../tmp', __FILE__)
56
+ FileUtils.rm_f(tmp+'/test_real.db')
57
+ FileUtils.cp(tmp+'/test.db', tmp+'/test_real.db')
58
+
59
+ ActiveRecord::Base.establish_connection({
60
+ :adapter => 'sqlite3',
61
+ :database => tmp+'/test_real.db'
62
+ })
63
+ end
64
+
65
+ Blackbird.options[:verbose] = false
66
+ Blackbird.options[:processors] = Blackbird::ProcessorList.new
67
+ Blackbird.options[:processors].use 'Blackbird::Processors::IndexedColumns'
68
+ Blackbird.options[:processors].use 'Blackbird::Processors::NormalDefault'
69
+
70
+ FRAGMENT_PATHS = (
71
+ Dir.glob(File.expand_path('../fixtures/a/**/*_fragment.rb', __FILE__)) +
72
+ Dir.glob(File.expand_path('../fixtures/b/**/*_fragment.rb', __FILE__)) )