dm-migrations 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,4 @@
1
+ dm-migrations
2
+ =============
3
+
4
+ DataMapper plugin for writing and specing migrations.
@@ -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,8 @@
1
+ TODO
2
+ ====
3
+
4
+
5
+
6
+ ---
7
+ TODO tickets may also be found in the DataMapper Issue Tracker:
8
+ http://wm.lighthouseapp.com/projects/4819-datamapper/overview
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + '/migration'
@@ -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