blackbird 1.0.0.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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__)) )
|