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,64 @@
|
|
1
|
+
class Blackbird::Patch
|
2
|
+
|
3
|
+
def initialize(fragment, name, options={}, &block)
|
4
|
+
@fragment = fragment
|
5
|
+
@name = name.to_s
|
6
|
+
@options = options.dup
|
7
|
+
@block = block
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :fragment, :name, :options, :block, :instructions
|
11
|
+
|
12
|
+
def call(changes)
|
13
|
+
@instructions = []
|
14
|
+
@block.call(SchemaPatcher.new(self, changes))
|
15
|
+
end
|
16
|
+
|
17
|
+
def <<(instruction)
|
18
|
+
@instructions << instruction
|
19
|
+
end
|
20
|
+
|
21
|
+
class SchemaPatcher
|
22
|
+
|
23
|
+
def initialize(patch, changes)
|
24
|
+
@patch, @changes = patch, changes
|
25
|
+
@instructions = []
|
26
|
+
end
|
27
|
+
|
28
|
+
%w( add? exists? remove? change? ).each do |m|
|
29
|
+
define_method(m) { |*args| @changes.__send__(m, *args) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def table(name)
|
33
|
+
(@tables ||= {})[name.to_s] = TablePatcher.new(@patch, @changes.table(name))
|
34
|
+
end
|
35
|
+
|
36
|
+
def apply(name)
|
37
|
+
@patch << [:apply, @patch.fragment.method(name)]
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
class TablePatcher
|
43
|
+
|
44
|
+
def initialize(patch, changes)
|
45
|
+
@patch, @changes = patch, changes
|
46
|
+
end
|
47
|
+
|
48
|
+
%w( add? add_index? remove? remove_index? change? change_index? exists? index_exists? ).each do |m|
|
49
|
+
define_method(m) { |*args| @changes.__send__(m, *args) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def apply(name)
|
53
|
+
@patch << [:apply, @patch.fragment.method(name)]
|
54
|
+
end
|
55
|
+
|
56
|
+
def rename(old_name, new_name)
|
57
|
+
@changes.old_columns.delete(old_name.to_s)
|
58
|
+
@changes.new_columns.delete(new_name.to_s)
|
59
|
+
@patch << [:rename_column, @changes.current.name, old_name.to_sym, new_name.to_sym]
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Blackbird::ProcessorList < Array
|
2
|
+
|
3
|
+
def use(processor, *args, &block)
|
4
|
+
push([processor.to_s, args, block])
|
5
|
+
self
|
6
|
+
end
|
7
|
+
|
8
|
+
def insert_before(other, processor, *args, &block)
|
9
|
+
idx = index { |p| p.first == other.to_s }
|
10
|
+
if idx
|
11
|
+
insert(idx, [processor.to_s, args, block])
|
12
|
+
else
|
13
|
+
unshift([processor.to_s, args, block])
|
14
|
+
end
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def insert_after(other, processor, *args, &block)
|
19
|
+
idx = index { |p| p.first == other.to_s }
|
20
|
+
if idx
|
21
|
+
insert(idx + 1, [processor.to_s, args, block])
|
22
|
+
else
|
23
|
+
push([processor.to_s, args, block])
|
24
|
+
end
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete(name)
|
29
|
+
idx = index { |p| p.first == other.to_s }
|
30
|
+
if idx
|
31
|
+
delete_at(idx)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def build
|
36
|
+
collect do |(klassname, args, block)|
|
37
|
+
klassname.constantize.new(*args, &block)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Blackbird::Processors::IndexedColumns
|
2
|
+
|
3
|
+
def visit_table(table)
|
4
|
+
@current_table = table
|
5
|
+
end
|
6
|
+
|
7
|
+
def visit_column(column)
|
8
|
+
options = column.options
|
9
|
+
|
10
|
+
if options.delete(:unique)
|
11
|
+
index_columns = [options.delete(:scope)].flatten.compact
|
12
|
+
index_columns << column.name
|
13
|
+
@current_table.add_index(index_columns, :unique => true)
|
14
|
+
end
|
15
|
+
|
16
|
+
if options.delete(:index)
|
17
|
+
@current_table.add_index(column.name)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
class Blackbird::Railtie < Rails::Railtie
|
2
|
+
|
3
|
+
config.blackbird = ActiveSupport::OrderedOptions.new
|
4
|
+
config.blackbird.verbose = true
|
5
|
+
config.blackbird.auto_run = false
|
6
|
+
config.blackbird.fragments = nil
|
7
|
+
|
8
|
+
config.generators.orm :active_record, :migration => false, :timestamps => true
|
9
|
+
|
10
|
+
rake_tasks do
|
11
|
+
load "blackbird/railtie/tasks.rake"
|
12
|
+
end
|
13
|
+
|
14
|
+
initializer "blackbird.setup_configuration" do |app|
|
15
|
+
Blackbird.options[:verbose] = app.config.blackbird.verbose
|
16
|
+
end
|
17
|
+
|
18
|
+
initializer "blackbird.find_fragments" do |app|
|
19
|
+
unless config.blackbird.fragments
|
20
|
+
config.blackbird.fragments = []
|
21
|
+
railties = [app.railties.all, app].flatten
|
22
|
+
railties.each do |railtie|
|
23
|
+
next unless railtie.respond_to? :paths
|
24
|
+
config.blackbird.fragments.concat(
|
25
|
+
railtie.paths.app.fragments.to_a)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
initializer "blackbird.run_blackbird" do |app|
|
31
|
+
if app.config.blackbird.auto_run
|
32
|
+
Blackbird::Transition.run!(
|
33
|
+
config.blackbird.fragments)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
generators do
|
38
|
+
require "rails/generators/active_record/transition/transition_generator"
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
class Rails::Engine::Configuration
|
44
|
+
|
45
|
+
alias_method :paths_without_blackbird, :paths
|
46
|
+
|
47
|
+
def paths
|
48
|
+
@paths ||= begin
|
49
|
+
paths_without_blackbird
|
50
|
+
@paths.app.fragments "app/schema", :glob => "**/*_fragment.rb"
|
51
|
+
@paths
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Blackbird::Schema
|
2
|
+
|
3
|
+
require 'blackbird/schema/loader'
|
4
|
+
require 'blackbird/schema/builder'
|
5
|
+
require 'blackbird/schema/changes'
|
6
|
+
|
7
|
+
attr_reader :tables, :patches
|
8
|
+
attr_writer :patches
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@tables = {}
|
12
|
+
@patches = ActiveSupport::OrderedHash.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def process(visitor)
|
16
|
+
if visitor.respond_to?(:visit_schema)
|
17
|
+
visitor.visit_schema(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
@tables.values.dup.each do |table|
|
21
|
+
table.process(visitor)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class Blackbird::Schema::Builder
|
2
|
+
|
3
|
+
def initialize(schema)
|
4
|
+
@schema = schema
|
5
|
+
end
|
6
|
+
|
7
|
+
def table(fragment, name, options={}, &block)
|
8
|
+
name = name.to_s
|
9
|
+
table = @schema.tables[name] || begin
|
10
|
+
@schema.tables[name] = Blackbird::Table.new(name, options)
|
11
|
+
end
|
12
|
+
|
13
|
+
if block
|
14
|
+
builder = Blackbird::Table::Builder.new(@schema, fragment, table)
|
15
|
+
if block.arity != 1
|
16
|
+
builder.instance_eval(&block)
|
17
|
+
else
|
18
|
+
block.call(builder)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def patch(fragment, name, options={}, &block)
|
26
|
+
patch = Blackbird::Patch.new(fragment, name, options, &block)
|
27
|
+
@schema.patches[patch.name] = patch
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
class Blackbird::Schema::Changes
|
2
|
+
|
3
|
+
def self.analyze!(current, future)
|
4
|
+
new(current, future).analyze!
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :new_tables, :old_tables, :changed_tables, :unchanged_tables
|
8
|
+
attr_reader :new_patches, :old_patches
|
9
|
+
|
10
|
+
def initialize(current, future)
|
11
|
+
@current, @future = current, future
|
12
|
+
end
|
13
|
+
|
14
|
+
def analyze!
|
15
|
+
@table_changes = (@current.tables.keys | @future.tables.keys).uniq
|
16
|
+
@table_changes = @table_changes.inject({}) do |memo, table_name|
|
17
|
+
memo[table_name] ||= Blackbird::Table::Changes.analyze!(
|
18
|
+
@current.tables[table_name], @future.tables[table_name])
|
19
|
+
memo
|
20
|
+
end
|
21
|
+
|
22
|
+
@new_tables = (@future.tables.keys - @current.tables.keys).sort
|
23
|
+
@old_tables = (@current.tables.keys - @future.tables.keys).sort
|
24
|
+
@changed_tables = @table_changes.inject([]) do |memo, (table_name, table)|
|
25
|
+
if @new_tables.include?(table_name)
|
26
|
+
memo
|
27
|
+
elsif @old_tables.include?(table_name)
|
28
|
+
memo
|
29
|
+
elsif table.has_changes?
|
30
|
+
memo << table_name
|
31
|
+
memo
|
32
|
+
else
|
33
|
+
memo
|
34
|
+
end
|
35
|
+
end.sort
|
36
|
+
@unchanged_tables = (@table_changes.keys -
|
37
|
+
(@changed_tables + @new_tables + @old_tables)).sort
|
38
|
+
|
39
|
+
@new_patches = @future.patches.keys - @current.patches
|
40
|
+
@old_patches = @current.patches - @future.patches.keys
|
41
|
+
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def table(name)
|
46
|
+
@table_changes[name.to_s]
|
47
|
+
end
|
48
|
+
|
49
|
+
def add?(name)
|
50
|
+
@new_tables.include?(name.to_s)
|
51
|
+
end
|
52
|
+
|
53
|
+
def remove?(name)
|
54
|
+
@old_tables.include?(name.to_s)
|
55
|
+
end
|
56
|
+
|
57
|
+
def change?(name)
|
58
|
+
@changed_tables.include?(name.to_s)
|
59
|
+
end
|
60
|
+
|
61
|
+
def unchange?(name)
|
62
|
+
@unchanged_tables.include?(name.to_s)
|
63
|
+
end
|
64
|
+
|
65
|
+
def exists?(name)
|
66
|
+
@table_changes.key?(name.to_s)
|
67
|
+
end
|
68
|
+
|
69
|
+
def should_apply?(patch)
|
70
|
+
@new_patches.include?(patch)
|
71
|
+
end
|
72
|
+
|
73
|
+
def should_forget?(patch)
|
74
|
+
@old_patches.include?(patch)
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
class Blackbird::Schema::Loader
|
2
|
+
|
3
|
+
def self.load
|
4
|
+
new.load
|
5
|
+
end
|
6
|
+
|
7
|
+
def load
|
8
|
+
schema = Blackbird::Schema.new
|
9
|
+
builder = Blackbird::Schema::Builder.new(schema)
|
10
|
+
|
11
|
+
schema.patches = []
|
12
|
+
|
13
|
+
connection.tables.each do |table|
|
14
|
+
|
15
|
+
if table == 'blackbird_patches'
|
16
|
+
schema.patches = connection.select_values(
|
17
|
+
%{ SELECT name FROM blackbird_patches })
|
18
|
+
end
|
19
|
+
|
20
|
+
pk_name = connection.primary_key(table)
|
21
|
+
has_pk = !!pk_name
|
22
|
+
|
23
|
+
builder.table(nil, table, :id => has_pk, :primary_key => pk_name) do |t|
|
24
|
+
|
25
|
+
connection.columns(table).each do |column|
|
26
|
+
next if column.name == pk_name
|
27
|
+
|
28
|
+
options = {}
|
29
|
+
|
30
|
+
if !column.null.nil?
|
31
|
+
options[:null] = column.null
|
32
|
+
else
|
33
|
+
options[:null] = true
|
34
|
+
end
|
35
|
+
|
36
|
+
if column.limit and column.limit != 255
|
37
|
+
options[:limit] = column.limit
|
38
|
+
end
|
39
|
+
|
40
|
+
if column.has_default?
|
41
|
+
options[:default] = column.default
|
42
|
+
end
|
43
|
+
|
44
|
+
if column.type == :decimal
|
45
|
+
options[:scale] = column.scale
|
46
|
+
options[:precision] = column.precision
|
47
|
+
end
|
48
|
+
|
49
|
+
t.column(column.name, column.type, options)
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
connection.indexes(table).each do |index|
|
54
|
+
|
55
|
+
options = {
|
56
|
+
:name => index.name
|
57
|
+
}
|
58
|
+
|
59
|
+
if index.unique
|
60
|
+
options[:unique] = index.unique
|
61
|
+
end
|
62
|
+
|
63
|
+
t.index(index.columns, options)
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
schema
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def connection
|
76
|
+
ActiveRecord::Base.connection
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
class Blackbird::Table
|
2
|
+
|
3
|
+
require 'blackbird/table/builder'
|
4
|
+
require 'blackbird/table/changes'
|
5
|
+
|
6
|
+
attr_reader :name, :options, :columns, :indexes
|
7
|
+
|
8
|
+
def initialize(name, options={})
|
9
|
+
@name = name.to_s
|
10
|
+
@options = { :id => true, :primary_key => 'id' }.merge(options)
|
11
|
+
@columns = ActiveSupport::OrderedHash.new
|
12
|
+
@indexes = ActiveSupport::OrderedHash.new
|
13
|
+
|
14
|
+
if @options.delete(:id) and pk_name = @options.delete(:primary_key)
|
15
|
+
add_column(pk_name, :integer, :primary => true)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def process(visitor)
|
20
|
+
if visitor.respond_to?(:visit_table)
|
21
|
+
visitor.visit_table(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
@columns.values.dup.each do |column|
|
25
|
+
column.process(visitor)
|
26
|
+
end
|
27
|
+
|
28
|
+
@indexes.values.dup.each do |index|
|
29
|
+
index.process(visitor)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def hash
|
34
|
+
[@name, @options, @columns, @indexes].hash
|
35
|
+
end
|
36
|
+
|
37
|
+
def primary_key
|
38
|
+
@columns.values.each do |column|
|
39
|
+
return column.name if column.primary?
|
40
|
+
end
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_column(name, type, options={})
|
45
|
+
column = Blackbird::Column.new(name, type, options)
|
46
|
+
@columns[column.name] = column
|
47
|
+
end
|
48
|
+
|
49
|
+
def remove_column(name)
|
50
|
+
column = @columns.delete(name.to_s)
|
51
|
+
@indexes.values.each do |index|
|
52
|
+
index.columns.delete(name.to_s)
|
53
|
+
remove_index(index.options[:name]) if index.columns.empty?
|
54
|
+
end
|
55
|
+
column
|
56
|
+
end
|
57
|
+
|
58
|
+
def change_column(name, type, options={})
|
59
|
+
@columns[name.to_s].change(type, options)
|
60
|
+
end
|
61
|
+
|
62
|
+
def add_index(columns, options={})
|
63
|
+
index = Blackbird::Index.new(@name, columns, options)
|
64
|
+
@indexes[index.name] = index
|
65
|
+
end
|
66
|
+
|
67
|
+
def remove_index(name)
|
68
|
+
@indexes.delete(name.to_s)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|