blackbird 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
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
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ doc/
5
+ tmp/
6
+ coverage
7
+ .yardoc
8
+ .DS_Store
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ ## 0.1.0 (Not released yet)
2
+
3
+ Features:
4
+
5
+ - [none]
6
+
7
+ Bugfixes:
8
+
9
+ - [none]
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source :rubygems
2
+ gemspec
3
+
4
+ group :development do
5
+ gem "shoulda", ">= 2.10.3"
6
+ end
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010 Simon Menke
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # Blackbird
2
+
3
+ Blackbird aims to remove traditional ActiveRecord migrations and replace it with DataMapper-like automatic migrations mixed with event based data patching.
4
+
5
+ The major advantage of Blackbird over migrations is that it allows Rails engines to define there own schemas which get automatically loaded in the application. The application remains in full control as it can overwrite an engines schema.
6
+
7
+ ## Installation
8
+
9
+ ### Using Ruby gems
10
+
11
+ Add the gem to your `Gemfile`
12
+
13
+ gem "rails", "3.0.0.beta3"
14
+ gem "blackbird"
15
+
16
+ ### Using git
17
+
18
+ Add the repo to your `Gemfile`
19
+
20
+ gem "blackbird", :git => "git://github.com/fd/blackbird.git"
21
+
22
+ ## Quick Start Guide
23
+
24
+ Generate a `Post` model without the migration file.
25
+
26
+ $ rails g model Post --migration=false
27
+
28
+ Create a schema file at `app/schemas/posts_schema.rb` with the following contents:
29
+
30
+ class PostsSchema < Blackbird::Schema
31
+
32
+ table :posts do |t|
33
+ t.string :title
34
+ t.text :body
35
+
36
+ t.datetime :published_at
37
+ t.timestamps
38
+ end
39
+
40
+ end
41
+
42
+ Then transition the database to the new schema:
43
+
44
+ $ rake db:transition
45
+ --- Creating table posts
46
+ +c title:string
47
+ +c body:text
48
+ +c published_at:datetime
49
+ +c created_at:datetime
50
+ +c updated_at:datetime
51
+
52
+ Now, let's make some trivial changes:
53
+
54
+ - add an index to the published_at column
55
+ - add an extra column for the tags
56
+
57
+ and here is the diff:
58
+
59
+ --- a/app/schemas/posts_schema.rb
60
+ +++ b/app/schemas/posts_schema.rb
61
+ @@ -4,7 +4,9 @@ class PostsSchema < Blackbird::Schema
62
+ t.string :title
63
+ t.text :body
64
+
65
+ - t.datetime :published_at
66
+ + t.string :tags
67
+ +
68
+ + t.datetime :published_at, :index => true
69
+ t.timestamps
70
+ end
71
+
72
+ Transition the database to the updated schema:
73
+
74
+ $ rake db:transition
75
+ --- Constructive changes for posts
76
+ +c tags:string
77
+ +i published_at
data/ROADMAP.md ADDED
@@ -0,0 +1,5 @@
1
+ ## 0.1.0
2
+
3
+ - non destructive blackbird
4
+ - automatic blackbird (at application boot)
5
+ - rails railtie
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'spec/rake/spectask'
5
+ Spec::Rake::SpecTask.new(:spec) do |t|
6
+ t.spec_files = FileList['spec/**/*_spec.rb']
7
+ t.spec_opts = ['-O', 'spec/spec.opts']
8
+ end
9
+
10
+ require 'yard'
11
+ YARD::Rake::YardocTask.new do |t|
12
+ t.files = ['lib/**/*.rb']
13
+ end
14
+
15
+ task :default => :spec
data/blackbird.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path("../lib/blackbird/version", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "blackbird"
6
+ s.version = Blackbird::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ["Simon Menke"]
9
+ s.email = ["simon.menke@gmail.com"]
10
+ s.homepage = "http://github.com/fd/blackbird"
11
+ s.summary = "Migrations should be more adaptable"
12
+ s.description = "Blackbird are Migrations but then better."
13
+
14
+ s.required_rubygems_version = ">= 1.3.6"
15
+ s.rubyforge_project = "blackbird"
16
+
17
+ s.add_runtime_dependency "activerecord", ">= 2.3.4"
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
21
+ s.require_path = 'lib'
22
+ end
data/lib/blackbird.rb ADDED
@@ -0,0 +1,39 @@
1
+ module Blackbird
2
+
3
+ require 'active_record'
4
+
5
+ require 'blackbird/version'
6
+
7
+ require 'blackbird/transition'
8
+ require 'blackbird/migration'
9
+ require 'blackbird/processor_list'
10
+
11
+ require 'blackbird/fragment'
12
+ require 'blackbird/schema'
13
+ require 'blackbird/table'
14
+ require 'blackbird/column'
15
+ require 'blackbird/index'
16
+ require 'blackbird/patch'
17
+
18
+ module Processors
19
+ require 'blackbird/processors/indexed_columns'
20
+ require 'blackbird/processors/normal_default'
21
+ end
22
+
23
+ module Helpers
24
+ require 'blackbird/helpers/typed_columns'
25
+ require 'blackbird/helpers/join_tables'
26
+ end
27
+
28
+ if defined? ::Rails::Railtie
29
+ require 'blackbird/railtie'
30
+ end
31
+
32
+ def self.options
33
+ @options ||= {
34
+ :verbose => true,
35
+ :processors => Blackbird::ProcessorList.new
36
+ }
37
+ end
38
+
39
+ end
@@ -0,0 +1,32 @@
1
+ class Blackbird::Column
2
+
3
+ attr_reader :name, :type, :options
4
+
5
+ def initialize(name, type, options={})
6
+ options.delete(:default) if options[:default] == nil
7
+ @name, @type, @options = name.to_s, type, options
8
+ end
9
+
10
+ def process(visitor)
11
+ if visitor.respond_to?(:visit_column)
12
+ visitor.visit_column(self)
13
+ end
14
+ end
15
+
16
+ def primary?
17
+ !!@options[:primary]
18
+ end
19
+
20
+ def hash
21
+ [@name, @type, @options].hash
22
+ end
23
+
24
+ def change(type, options={})
25
+ @type, @options = type, options
26
+ end
27
+
28
+ def ==(other)
29
+ self.hash == other.hash
30
+ end
31
+
32
+ end
@@ -0,0 +1,58 @@
1
+ class Blackbird::Fragment
2
+
3
+ def self.subclasses
4
+ @subclasses ||= []
5
+ end
6
+
7
+ def self.inherited(subclass)
8
+ Blackbird::Fragment.subclasses.push(subclass)
9
+ subclass.extend InheritanceBlocker
10
+ end
11
+
12
+ def self.instructions
13
+ @instructions ||= []
14
+ end
15
+
16
+ def self.table(name, options={}, &block)
17
+ self.instructions << [:table, name, options, block]
18
+ end
19
+
20
+ def self.patch(name, options={}, &block)
21
+ self.instructions << [:patch, name, options, block]
22
+ end
23
+
24
+ def initialize
25
+ @instructions = self.class.instructions.dup
26
+ end
27
+
28
+ def apply(builder)
29
+ @instructions.each do |instruction|
30
+ instruction = instruction.dup
31
+ instruction[1,0] = self
32
+ block = (Proc === instruction.last ? instruction.pop : nil)
33
+ builder.__send__(*instruction, &block)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def connection
40
+ ActiveRecord::Base.connection
41
+ end
42
+
43
+ %w( select_values select_value select_rows select_all execute ).each do |m|
44
+ define_method(m) do |sql_pattern, *args|
45
+ sql = sql = sql_pattern.gsub('?') do
46
+ connection.quote(arguments.shift)
47
+ end
48
+ connection.__send__(m, sql)
49
+ end
50
+ end
51
+
52
+ module InheritanceBlocker
53
+ def inherited(base)
54
+ raise "#{base} cannot inherit from non abstract fragment #{self}"
55
+ end
56
+ end
57
+
58
+ end
@@ -0,0 +1,50 @@
1
+ module Blackbird::Helpers::JoinTables
2
+
3
+ def join(*args, &block)
4
+
5
+ tables = []
6
+ columns = []
7
+
8
+ while arg = args.shift
9
+ case arg
10
+ when String, Symbol
11
+ tables << arg.to_s
12
+ columns << "#{arg.to_s.singularize}_id"
13
+
14
+ when Array
15
+ tables << arg[0].to_s
16
+ columns << arg[1].to_s
17
+
18
+ when Hash
19
+ args = arg.to_ary + args
20
+
21
+ else
22
+ raise ArgumentError
23
+
24
+ end
25
+ end
26
+
27
+ name = tables.dup.sort.join('_')
28
+
29
+ table name, :id => false do |t|
30
+
31
+ columns.each do |column|
32
+ t.integer column
33
+ end
34
+
35
+ if block
36
+ if block.arity == 1
37
+ block.call(t)
38
+ else
39
+ t.instance_eval(&block)
40
+ end
41
+ end
42
+
43
+ end
44
+
45
+ self
46
+ end
47
+
48
+ Blackbird::Schema::Builder.send(:include, self)
49
+
50
+ end
@@ -0,0 +1,50 @@
1
+ module Blackbird::Helpers::TypedColumns
2
+
3
+ def integer(name, options={})
4
+ column(name, :integer, options)
5
+ end
6
+
7
+ def float(name, options={})
8
+ column(name, :float, options)
9
+ end
10
+
11
+ def decimal(name, options={})
12
+ column(name, :decimal, options)
13
+ end
14
+
15
+ def string(name, options={})
16
+ column(name, :string, options)
17
+ end
18
+
19
+ def text(name, options={})
20
+ column(name, :text, options)
21
+ end
22
+
23
+ def boolean(name, options={})
24
+ column(name, :boolean, options)
25
+ end
26
+
27
+ def binary(name, options={})
28
+ column(name, :binary, options)
29
+ end
30
+
31
+ def datetime(name, options={})
32
+ column(name, :datetime, options)
33
+ end
34
+
35
+ def date(name, options={})
36
+ column(name, :date, options)
37
+ end
38
+
39
+ def timestamp(name, options={})
40
+ column(name, :timestamp, options)
41
+ end
42
+
43
+ def timestamps
44
+ datetime :created_at
45
+ datetime :updated_at
46
+ end
47
+
48
+ Blackbird::Table::Builder.send(:include, self)
49
+
50
+ end
@@ -0,0 +1,29 @@
1
+ class Blackbird::Index
2
+
3
+ attr_reader :name, :columns, :options
4
+
5
+ def initialize(table_name, columns, options={})
6
+ @table_name = table_name
7
+ @columns, @options = [columns].flatten.compact, options
8
+ @columns.collect! { |n| n.to_s }
9
+ end
10
+
11
+ def process(visitor)
12
+ if visitor.respond_to?(:visit_index)
13
+ visitor.visit_index(self)
14
+ end
15
+ end
16
+
17
+ def name
18
+ @options[:name] ||= "index_#{@table_name}_on_#{Array(@columns) * '_and_'}"
19
+ end
20
+
21
+ def hash
22
+ [@columns, @options].hash
23
+ end
24
+
25
+ def ==(other)
26
+ self.hash == other.hash
27
+ end
28
+
29
+ end
@@ -0,0 +1,218 @@
1
+ # Blackbird::Migration is responsible for creating database instructions
2
+ class Blackbird::Migration
3
+
4
+ attr_reader :instructions
5
+
6
+ def self.build(current, future, changes)
7
+ new(current, future, changes).build
8
+ end
9
+
10
+ def initialize(current, future, changes)
11
+ @current, @future, @changes = current, future, changes
12
+ @instructions = []
13
+ end
14
+
15
+ def build
16
+ evaluate_patches
17
+
18
+ remove_old_indexes
19
+
20
+ create_new_tables
21
+ change_existing_tables
22
+ remove_old_tables
23
+
24
+ create_new_indexes
25
+
26
+ self
27
+ end
28
+
29
+ private
30
+
31
+ def create_new_tables
32
+ @changes.new_tables.each do |table_name|
33
+ table = @future.tables[table_name]
34
+
35
+ pk_name = table.primary_key
36
+ has_pk = !!pk_name
37
+
38
+ log "--- Creating table #{table_name}"
39
+ run :create_table, table_name, table.options.merge(:id => has_pk, :primary_key => pk_name)
40
+
41
+ table.columns.each do |name, column|
42
+ next if name == pk_name
43
+
44
+ log " +c #{name}:#{column.type}"
45
+ run :add_column, table_name, name, column.type, column.options
46
+ end
47
+ end
48
+ end
49
+
50
+ def evaluate_patches
51
+ @evaluated_patches = []
52
+ @changes.new_patches.each do |patch_name|
53
+
54
+ patch = @future.patches[patch_name]
55
+ patch.call(@changes)
56
+ @evaluated_patches << patch
57
+
58
+ end
59
+ end
60
+
61
+ def apply_patches
62
+ @evaluated_patches.each do |patch|
63
+
64
+ log "--- Applying patch #{patch.name}"
65
+ @instructions.concat(patch.instructions)
66
+
67
+ end
68
+ end
69
+
70
+ def change_existing_tables
71
+ # new columns
72
+ @changes.changed_tables.each do |table_name|
73
+ changes = @changes.table(table_name)
74
+ future_table = @future.tables[table_name]
75
+
76
+ unless changes.new_columns.empty?
77
+ log "--- Constructive changes for #{table_name}"
78
+ end
79
+
80
+ changes.new_columns.each do |name|
81
+ column = future_table.columns[name]
82
+
83
+ log " +c #{name}:#{column.type}"
84
+ run :add_column, table_name, name, column.type, column.options
85
+ end
86
+ end
87
+
88
+ apply_patches
89
+
90
+ # column changes
91
+ @changes.changed_tables.each do |table_name|
92
+ changes = @changes.table(table_name)
93
+ current_table = @current.tables[table_name]
94
+ future_table = @future.tables[table_name]
95
+
96
+ unless changes.changed_columns.empty?
97
+ log "--- Mutative changes for #{table_name}"
98
+ end
99
+
100
+ changes.changed_columns.each do |name|
101
+ column = future_table.columns[name]
102
+
103
+ log " ~c #{name}:#{column.type} #{current_table.columns[name].options.inspect} => #{column.options.inspect}"
104
+ run :change_column, table_name, name, column.type, column.options
105
+ end
106
+ end
107
+
108
+ # old columns
109
+ @changes.changed_tables.each do |table_name|
110
+ changes = @changes.table(table_name)
111
+ current_table = @current.tables[table_name]
112
+
113
+ unless changes.old_columns.empty?
114
+ log "--- Destructive changes for #{table_name}"
115
+ end
116
+
117
+ changes.old_columns.each do |name|
118
+ column = current_table.columns[name]
119
+
120
+ log " -c #{name}:#{column.type}"
121
+ run :remove_column, table_name, name
122
+ end
123
+ end
124
+ end
125
+
126
+ def remove_old_tables
127
+ @changes.old_tables.each do |table_name|
128
+ log "-- Dropping table #{table_name}"
129
+ run :drop_table, table_name
130
+ end
131
+ end
132
+
133
+ def remove_old_indexes
134
+ @changes.changed_tables.each do |table_name|
135
+ changes = @changes.table(table_name)
136
+ current_table = @current.tables[table_name]
137
+
138
+ changes.old_indexes.each do |name|
139
+ index = current_table.indexes[name]
140
+
141
+ log(index.options[:unique] ?
142
+ " -u #{index.columns * ' '}" :
143
+ " -i #{index.columns * ' '}" )
144
+ run :remove_index, table_name, {:name => name}
145
+ end
146
+ end
147
+
148
+ @changes.changed_tables.each do |table_name|
149
+ changes = @changes.table(table_name)
150
+ future_table = @future.tables[table_name]
151
+
152
+ changes.changed_indexes.each do |name|
153
+ index = future_table.indexes[name]
154
+
155
+ log(index.options[:unique] ?
156
+ " ~u #{index.columns * ' '}" :
157
+ " ~i #{index.columns * ' '}" )
158
+
159
+ run :remove_index, table_name, {:name => name}
160
+ end
161
+ end
162
+ end
163
+
164
+ def create_new_indexes
165
+ @changes.changed_tables.each do |table_name|
166
+ changes = @changes.table(table_name)
167
+ future_table = @future.tables[table_name]
168
+
169
+ changes.changed_indexes.each do |name|
170
+ index = future_table.indexes[name]
171
+
172
+ log(index.options[:unique] ?
173
+ " ~u #{index.columns * ' '}" :
174
+ " ~i #{index.columns * ' '}" )
175
+
176
+ run :add_index, table_name, index.columns, index.options
177
+ end
178
+ end
179
+
180
+ @changes.changed_tables.each do |table_name|
181
+ changes = @changes.table(table_name)
182
+ future_table = @future.tables[table_name]
183
+
184
+ changes.new_indexes.each do |name|
185
+ index = future_table.indexes[name]
186
+
187
+ log(index.options[:unique] ?
188
+ " +u #{index.columns * ' '}" :
189
+ " +i #{index.columns * ' '}" )
190
+
191
+ run :add_index, table_name, index.columns, index.options
192
+ end
193
+ end
194
+
195
+ @changes.new_tables.each do |table_name|
196
+ table = @future.tables[table_name]
197
+
198
+ table.indexes.each do |name, index|
199
+ log(index.options[:unique] ?
200
+ " +u #{index.columns * ' '}" :
201
+ " +i #{index.columns * ' '}" )
202
+
203
+ run :add_index, table_name, index.columns, index.options
204
+ end
205
+ end
206
+ end
207
+
208
+ def run(*instruction)
209
+ @instructions << instruction
210
+ end
211
+
212
+ def log(message)
213
+ if Blackbird.options[:verbose]
214
+ run :log, message
215
+ end
216
+ end
217
+
218
+ end