blackbird 1.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +6 -0
- data/LICENSE +19 -0
- data/README.md +77 -0
- data/ROADMAP.md +5 -0
- data/Rakefile +15 -0
- data/blackbird.gemspec +22 -0
- data/lib/blackbird.rb +39 -0
- data/lib/blackbird/column.rb +32 -0
- data/lib/blackbird/fragment.rb +58 -0
- data/lib/blackbird/helpers/join_tables.rb +50 -0
- data/lib/blackbird/helpers/typed_columns.rb +50 -0
- data/lib/blackbird/index.rb +29 -0
- data/lib/blackbird/migration.rb +218 -0
- data/lib/blackbird/patch.rb +64 -0
- data/lib/blackbird/processor_list.rb +41 -0
- data/lib/blackbird/processors/indexed_columns.rb +22 -0
- data/lib/blackbird/processors/normal_default.rb +7 -0
- data/lib/blackbird/railtie.rb +55 -0
- data/lib/blackbird/railtie/tasks.rake +8 -0
- data/lib/blackbird/schema.rb +25 -0
- data/lib/blackbird/schema/builder.rb +31 -0
- data/lib/blackbird/schema/changes.rb +77 -0
- data/lib/blackbird/schema/loader.rb +79 -0
- data/lib/blackbird/table.rb +71 -0
- data/lib/blackbird/table/builder.rb +124 -0
- data/lib/blackbird/table/changes.rb +86 -0
- data/lib/blackbird/transition.rb +85 -0
- data/lib/blackbird/version.rb +3 -0
- data/lib/rails/generators/active_record/transition/templates/fragment.rb +12 -0
- data/lib/rails/generators/active_record/transition/transition_generator.rb +25 -0
- data/spec/fixtures/a/comments_fragment.rb +10 -0
- data/spec/fixtures/a/pages_fragment.rb +9 -0
- data/spec/fixtures/a/posts_fragment.rb +9 -0
- data/spec/fixtures/a/users_fragment.rb +7 -0
- data/spec/fixtures/b/comments_fragment.rb +23 -0
- data/spec/fixtures/b/posts_fragment.rb +8 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +72 -0
- data/spec/transitions/migration_spec.rb +116 -0
- data/spec/transitions/schema_loader_spec.rb +35 -0
- 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,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,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
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -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__)) )
|