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,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__)) )