dm-migrations 0.9.2

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.
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