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.
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,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,7 @@
1
+ class Blackbird::Processors::NormalDefault
2
+
3
+ def visit_column(column)
4
+ column.options.delete(:default) if column.options[:default].nil?
5
+ end
6
+
7
+ 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,8 @@
1
+ namespace :db do
2
+
3
+ desc "Transition to the current schema"
4
+ task :transition => :environment do
5
+ Blackbird::Transition.run!(Rails.application.config.blackbird.fragments)
6
+ end
7
+
8
+ 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