dm-migrations 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README +4 -0
- data/Rakefile +88 -0
- data/TODO +8 -0
- data/lib/dm-migrations.rb +1 -0
- data/lib/migration.rb +192 -0
- data/lib/migration_runner.rb +88 -0
- data/lib/spec/example/migration_example_group.rb +73 -0
- data/lib/spec/matchers/migration_matchers.rb +107 -0
- data/lib/sql.rb +110 -0
- data/lib/sql/column.rb +9 -0
- data/lib/sql/mysql.rb +52 -0
- data/lib/sql/postgresql.rb +78 -0
- data/lib/sql/sqlite3.rb +50 -0
- data/lib/sql/table.rb +19 -0
- data/spec/integration/migration_runner_spec.rb +78 -0
- data/spec/integration/migration_spec.rb +136 -0
- data/spec/integration/sql_spec.rb +148 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +37 -0
- metadata +82 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2007 Paul Sadauskas
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'spec'
|
3
|
+
require 'rake/clean'
|
4
|
+
require 'rake/gempackagetask'
|
5
|
+
require 'spec/rake/spectask'
|
6
|
+
require 'pathname'
|
7
|
+
|
8
|
+
CLEAN.include '{log,pkg}/'
|
9
|
+
|
10
|
+
spec = Gem::Specification.new do |s|
|
11
|
+
s.name = 'dm-migrations'
|
12
|
+
s.version = '0.9.2'
|
13
|
+
s.platform = Gem::Platform::RUBY
|
14
|
+
s.has_rdoc = true
|
15
|
+
s.extra_rdoc_files = %w[ README LICENSE TODO ]
|
16
|
+
s.summary = 'DataMapper plugin for writing and specing migrations'
|
17
|
+
s.description = s.summary
|
18
|
+
s.author = 'Paul Sadauskas'
|
19
|
+
s.email = 'psadauskas@gmail.com'
|
20
|
+
s.homepage = 'http://github.com/sam/dm-more/tree/master/dm-migrations'
|
21
|
+
s.require_path = 'lib'
|
22
|
+
s.files = FileList[ '{lib,spec}/**/*.rb', 'spec/spec.opts', 'Rakefile', *s.extra_rdoc_files ]
|
23
|
+
s.add_dependency('dm-core', "=#{s.version}")
|
24
|
+
end
|
25
|
+
|
26
|
+
task :default => [ :spec ]
|
27
|
+
|
28
|
+
WIN32 = (RUBY_PLATFORM =~ /win32|mingw|cygwin/) rescue nil
|
29
|
+
SUDO = WIN32 ? '' : ('sudo' unless ENV['SUDOLESS'])
|
30
|
+
|
31
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
32
|
+
pkg.gem_spec = spec
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "Install #{spec.name} #{spec.version} (default ruby)"
|
36
|
+
task :install => [ :package ] do
|
37
|
+
sh "#{SUDO} gem install --local pkg/#{spec.name}-#{spec.version} --no-update-sources", :verbose => false
|
38
|
+
end
|
39
|
+
|
40
|
+
desc "Uninstall #{spec.name} #{spec.version} (default ruby)"
|
41
|
+
task :uninstall => [ :clobber ] do
|
42
|
+
sh "#{SUDO} gem uninstall #{spec.name} -v#{spec.version} -I -x", :verbose => false
|
43
|
+
end
|
44
|
+
|
45
|
+
namespace :jruby do
|
46
|
+
desc "Install #{spec.name} #{spec.version} with JRuby"
|
47
|
+
task :install => [ :package ] do
|
48
|
+
sh %{#{SUDO} jruby -S gem install --local pkg/#{spec.name}-#{spec.version} --no-update-sources}, :verbose => false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
desc 'Run specifications'
|
53
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
54
|
+
t.spec_opts << '--options' << 'spec/spec.opts' if File.exists?('spec/spec.opts')
|
55
|
+
t.spec_files = Pathname.glob(Pathname.new(__FILE__).dirname + 'spec/**/*_spec.rb')
|
56
|
+
|
57
|
+
begin
|
58
|
+
t.rcov = ENV.has_key?('NO_RCOV') ? ENV['NO_RCOV'] != 'true' : true
|
59
|
+
t.rcov_opts << '--exclude' << 'spec'
|
60
|
+
t.rcov_opts << '--text-summary'
|
61
|
+
t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse'
|
62
|
+
rescue Exception
|
63
|
+
# rcov not installed
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
namespace :db do
|
68
|
+
|
69
|
+
# pass the relative path to the migrations directory by MIGRATION_DIR
|
70
|
+
task :setup_migration_dir do
|
71
|
+
unless defined?(MIGRATION_DIR)
|
72
|
+
migration_dir = ENV["MIGRATION_DIR"] || File.join("db", "migrations")
|
73
|
+
MIGRATION_DIR = File.expand_path(File.join(File.dirname(__FILE__), migration_dir))
|
74
|
+
end
|
75
|
+
FileUtils.mkdir_p MIGRATION_DIR
|
76
|
+
end
|
77
|
+
|
78
|
+
# set DIRECTION to migrate down
|
79
|
+
desc "Run your system's migrations"
|
80
|
+
task :migrate => [:setup_migration_dir] do
|
81
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "lib", "migration_runner.rb"))
|
82
|
+
require File.expand_path(File.join(MIGRATION_DIR, "config.rb"))
|
83
|
+
|
84
|
+
Dir[File.join(MIGRATION_DIR, "*.rb")].each { |file| require file }
|
85
|
+
|
86
|
+
ENV["DIRECTION"] != "down" ? migrate_up! : migrate_down!
|
87
|
+
end
|
88
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/migration'
|
data/lib/migration.rb
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'dm-core', '=0.9.2'
|
3
|
+
require 'dm-core'
|
4
|
+
require 'benchmark'
|
5
|
+
require File.dirname(__FILE__) + '/sql'
|
6
|
+
|
7
|
+
module DataMapper
|
8
|
+
class DuplicateMigrationNameError < StandardError
|
9
|
+
def initialize(migration)
|
10
|
+
super("Duplicate Migration Name: '#{migration.name}', version: #{migration.position}")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Migration
|
15
|
+
include SQL
|
16
|
+
|
17
|
+
attr_accessor :position, :name, :database, :adapter
|
18
|
+
|
19
|
+
def initialize( position, name, opts = {}, &block )
|
20
|
+
@position, @name = position, name
|
21
|
+
@options = opts
|
22
|
+
|
23
|
+
@database = DataMapper.repository(@options[:database] || :default)
|
24
|
+
@adapter = @database.adapter
|
25
|
+
|
26
|
+
case @adapter.class.to_s
|
27
|
+
when /Sqlite3/ then @adapter.extend(SQL::Sqlite3)
|
28
|
+
when /Mysql/ then @adapter.extend(SQL::Mysql)
|
29
|
+
when /Postgres/ then @adapter.extend(SQL::Postgresql)
|
30
|
+
else
|
31
|
+
raise "Unsupported Migration Adapter #{@adapter.class}"
|
32
|
+
end
|
33
|
+
|
34
|
+
@verbose = @options.has_key?(:verbose) ? @options[:verbose] : true
|
35
|
+
|
36
|
+
@up_action = lambda {}
|
37
|
+
@down_action = lambda {}
|
38
|
+
|
39
|
+
instance_eval &block
|
40
|
+
end
|
41
|
+
|
42
|
+
# define the actions that should be performed on an up migration
|
43
|
+
def up(&block)
|
44
|
+
@up_action = block
|
45
|
+
end
|
46
|
+
|
47
|
+
# define the actions that should be performed on a down migration
|
48
|
+
def down(&block)
|
49
|
+
@down_action = block
|
50
|
+
end
|
51
|
+
|
52
|
+
# perform the migration by running the code in the #up block
|
53
|
+
def perform_up
|
54
|
+
res = nil
|
55
|
+
if needs_up?
|
56
|
+
# DataMapper.database.adapter.transaction do
|
57
|
+
say_with_time "== Performing Up Migration ##{position}: #{name}", 0 do
|
58
|
+
res = @up_action.call
|
59
|
+
end
|
60
|
+
update_migration_info(:up)
|
61
|
+
# end
|
62
|
+
end
|
63
|
+
res
|
64
|
+
end
|
65
|
+
|
66
|
+
# un-do the migration by running the code in the #down block
|
67
|
+
def perform_down
|
68
|
+
res = nil
|
69
|
+
if needs_down?
|
70
|
+
# DataMapper.database.adapter.transaction do
|
71
|
+
say_with_time "== Performing Down Migration ##{position}: #{name}", 0 do
|
72
|
+
res = @down_action.call
|
73
|
+
end
|
74
|
+
update_migration_info(:down)
|
75
|
+
# end
|
76
|
+
end
|
77
|
+
res
|
78
|
+
end
|
79
|
+
|
80
|
+
# execute raw SQL
|
81
|
+
def execute(sql)
|
82
|
+
say_with_time(sql) do
|
83
|
+
@adapter.execute(sql)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def create_table(table_name, opts = {}, &block)
|
88
|
+
execute TableCreator.new(@adapter, table_name, opts, &block).to_sql
|
89
|
+
end
|
90
|
+
|
91
|
+
def drop_table(table_name, opts = {})
|
92
|
+
execute "DROP TABLE #{@adapter.send(:quote_table_name, table_name.to_s)}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def modify_table(table_name, opts = {}, &block)
|
96
|
+
TableModifier.new(@adapter, table_name, opts, &block).statements.each do |sql|
|
97
|
+
execute(sql)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Orders migrations by position, so we know what order to run them in.
|
102
|
+
# First order by postition, then by name, so at least the order is predictable.
|
103
|
+
def <=> other
|
104
|
+
if self.position == other.position
|
105
|
+
self.name.to_s <=> other.name.to_s
|
106
|
+
else
|
107
|
+
self.position <=> other.position
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Output some text. Optional indent level
|
112
|
+
def say(message, indent = 4)
|
113
|
+
write "#{" " * indent} #{message}"
|
114
|
+
end
|
115
|
+
|
116
|
+
# Time how long the block takes to run, and output it with the message.
|
117
|
+
def say_with_time(message, indent = 2)
|
118
|
+
say(message, indent)
|
119
|
+
result = nil
|
120
|
+
time = Benchmark.measure { result = yield }
|
121
|
+
say("-> %.4fs" % time.real, indent)
|
122
|
+
result
|
123
|
+
end
|
124
|
+
|
125
|
+
# output the given text, but only if verbose mode is on
|
126
|
+
def write(text="")
|
127
|
+
puts text if @verbose
|
128
|
+
end
|
129
|
+
|
130
|
+
protected
|
131
|
+
|
132
|
+
# Inserts or removes a row into the `migration_info` table, so we can mark this migration as run, or un-done
|
133
|
+
def update_migration_info(direction)
|
134
|
+
save, @verbose = @verbose, false
|
135
|
+
|
136
|
+
create_migration_info_table_if_needed
|
137
|
+
|
138
|
+
if direction.to_sym == :up
|
139
|
+
execute("INSERT INTO #{migration_info_table} (#{migration_name_column}) VALUES (#{quoted_name})")
|
140
|
+
elsif direction.to_sym == :down
|
141
|
+
execute("DELETE FROM #{migration_info_table} WHERE #{migration_name_column} = #{quoted_name}")
|
142
|
+
end
|
143
|
+
@verbose = save
|
144
|
+
end
|
145
|
+
|
146
|
+
def create_migration_info_table_if_needed
|
147
|
+
save, @verbose = @verbose, true # false
|
148
|
+
unless migration_info_table_exists?
|
149
|
+
execute("CREATE TABLE #{migration_info_table} (#{migration_name_column} VARCHAR(255) UNIQUE)")
|
150
|
+
end
|
151
|
+
@verbose = save
|
152
|
+
end
|
153
|
+
|
154
|
+
# Quote the name of the migration for use in SQL
|
155
|
+
def quoted_name
|
156
|
+
"'#{name}'"
|
157
|
+
end
|
158
|
+
|
159
|
+
def migration_info_table_exists?
|
160
|
+
adapter.storage_exists?('migration_info')
|
161
|
+
end
|
162
|
+
|
163
|
+
# Fetch the record for this migration out of the migration_info table
|
164
|
+
def migration_record
|
165
|
+
return [] unless migration_info_table_exists?
|
166
|
+
@adapter.query("SELECT #{migration_name_column} FROM #{migration_info_table} WHERE #{migration_name_column} = #{quoted_name}")
|
167
|
+
end
|
168
|
+
|
169
|
+
# True if the migration needs to be run
|
170
|
+
def needs_up?
|
171
|
+
create_migration_info_table_if_needed
|
172
|
+
migration_record.empty?
|
173
|
+
end
|
174
|
+
|
175
|
+
# True if the migration has already been run
|
176
|
+
def needs_down?
|
177
|
+
create_migration_info_table_if_needed
|
178
|
+
! migration_record.empty?
|
179
|
+
end
|
180
|
+
|
181
|
+
# Quoted table name, for the adapter
|
182
|
+
def migration_info_table
|
183
|
+
@migration_info_table ||= @adapter.send(:quote_table_name, 'migration_info')
|
184
|
+
end
|
185
|
+
|
186
|
+
# Quoted `migration_name` column, for the adapter
|
187
|
+
def migration_name_column
|
188
|
+
@migration_name_column ||= @adapter.send(:quote_column_name, 'migration_name')
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/migration'
|
2
|
+
|
3
|
+
module DataMapper
|
4
|
+
module MigrationRunner
|
5
|
+
@@migrations ||= []
|
6
|
+
|
7
|
+
# Creates a new migration, and adds it to the list of migrations to be run.
|
8
|
+
# Migrations can be defined in any order, they will be sorted and run in the
|
9
|
+
# correct order.
|
10
|
+
#
|
11
|
+
# The order that migrations are run in is set by the first argument. It is not
|
12
|
+
# neccessary that this be unique; migrations with the same version number are
|
13
|
+
# expected to be able to be run in any order.
|
14
|
+
#
|
15
|
+
# The second argument is the name of the migration. This name is used internally
|
16
|
+
# to track if the migration has been run. It is required that this name be unique
|
17
|
+
# across all migrations.
|
18
|
+
#
|
19
|
+
# Addtionally, it accepts a number of options:
|
20
|
+
# * <tt>:database</tt> If you defined several DataMapper::database instances use this
|
21
|
+
# to choose which one to run the migration gagainst. Defaults to <tt>:default</tt>.
|
22
|
+
# Migrations are tracked individually per database.
|
23
|
+
# * <tt>:verbose</tt> true/false, defaults to true. Determines if the migration should
|
24
|
+
# output its status messages when it runs.
|
25
|
+
#
|
26
|
+
# Example of a simple migration:
|
27
|
+
#
|
28
|
+
# migration( 1, :create_people_table ) do
|
29
|
+
# up do
|
30
|
+
# create_table :people do
|
31
|
+
# column :id, Integer, :serial => true
|
32
|
+
# column :name, String, :size => 50
|
33
|
+
# column :age, Integer
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
# down do
|
37
|
+
# drop_table :people
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# Its recommended that you stick with raw SQL for migrations that manipulate data. If
|
42
|
+
# you write a migration using a model, then later change the model, there's a
|
43
|
+
# possibility the migration will no longer work. Using SQL will always work.
|
44
|
+
def migration( number, name, opts = {}, &block )
|
45
|
+
@@migrations ||= []
|
46
|
+
raise "Migration name conflict: '#{name}'" if @@migrations.map { |m| m.name }.include?(name.to_s)
|
47
|
+
|
48
|
+
@@migrations << DataMapper::Migration.new( number, name.to_s, opts, &block )
|
49
|
+
end
|
50
|
+
|
51
|
+
# Run all migrations that need to be run. In most cases, this would be called by a
|
52
|
+
# rake task as part of a larger project, but this provides the ability to run them
|
53
|
+
# in a script or test.
|
54
|
+
#
|
55
|
+
# has an optional argument 'level' which if supplied, only performs the migrations
|
56
|
+
# with a position less than or equal to the level.
|
57
|
+
def migrate_up!(level = nil)
|
58
|
+
@@migrations.sort.each do |migration|
|
59
|
+
if level.nil?
|
60
|
+
migration.perform_up()
|
61
|
+
else
|
62
|
+
migration.perform_up() if migration.position <= level.to_i
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Run all the down steps for the migrations that have already been run.
|
68
|
+
#
|
69
|
+
# has an optional argument 'level' which, if supplied, only performs the
|
70
|
+
# down migrations with a postion greater than the level.
|
71
|
+
def migrate_down!(level = nil)
|
72
|
+
@@migrations.sort.reverse.each do |migration|
|
73
|
+
if level.nil?
|
74
|
+
migration.perform_down()
|
75
|
+
else
|
76
|
+
migration.perform_down() if migration.position > level.to_i
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def migrations
|
82
|
+
@@migrations
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
include DataMapper::MigrationRunner
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'spec'
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + '/../matchers/migration_matchers'
|
4
|
+
|
5
|
+
module Spec
|
6
|
+
module Example
|
7
|
+
class MigrationExampleGroup < Spec::Example::ExampleGroup
|
8
|
+
include Spec::Matchers::Migration
|
9
|
+
|
10
|
+
before(:all) do
|
11
|
+
if this_migration.adapter.supports_schema_transactions?
|
12
|
+
run_prereq_migrations
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
before(:each) do
|
17
|
+
if ! this_migration.adapter.supports_schema_transactions?
|
18
|
+
run_prereq_migrations
|
19
|
+
else
|
20
|
+
this_migration.adapter.begin_transaction
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
after(:each) do
|
25
|
+
if this_migration.adapter.supports_schema_transactions?
|
26
|
+
this_migration.adapter.rollback_transaction
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
after(:all) do
|
31
|
+
this_migration.adapter.recreate_database
|
32
|
+
end
|
33
|
+
|
34
|
+
def run_prereq_migrations
|
35
|
+
"running n-1 migrations"
|
36
|
+
all_databases.each do |db|
|
37
|
+
db.adapter.recreate_database
|
38
|
+
end
|
39
|
+
@@migrations.sort.each do |migration|
|
40
|
+
break if migration.name.to_s == migration_name.to_s
|
41
|
+
migration.perform_up
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def run_migration
|
46
|
+
this_migration.perform_up
|
47
|
+
end
|
48
|
+
|
49
|
+
def migration_name
|
50
|
+
@migration_name ||= self.class.instance_variable_get("@description_text").to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
def all_databases
|
54
|
+
@@migrations.map { |m| m.database }.uniq
|
55
|
+
end
|
56
|
+
|
57
|
+
def this_migration
|
58
|
+
@@migrations.select { |m| m.name.to_s == migration_name }.first
|
59
|
+
end
|
60
|
+
|
61
|
+
def query(sql)
|
62
|
+
this_migration.adapter.query(sql)
|
63
|
+
end
|
64
|
+
|
65
|
+
def table(table_name)
|
66
|
+
this_migration.adapter.table(table_name)
|
67
|
+
end
|
68
|
+
|
69
|
+
Spec::Example::ExampleGroupFactory.register(:migration, self)
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|