From 44c90cc62acfd89857a1e7e8dd4208ca06d7b3c9 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Fri, 7 May 2010 14:26:54 -0700 Subject: [PATCH] Add a migrator that works with timestamped migrations This commit splits Migrator into IntegerMigrator and TimestampMigrator, both subclasses of Migrator. Migrator maintains the same API (apply and run), and picks which subclass to use based on the migration file names. TimestampMigrator allows you to apply migrations out of order. I tried to handle the corner cases I could think of, such as: * Migrations with the same timestamp (Sequel allows this as they have different filenames) * Applied migrations that are not in the file system (Sequel raises an exception) * Migrating to a specific timestamp where you have to migrate some migrations up (because they haven't been applied and the timestamp is less than the target) and other migrations down (because they have been applied and the timestamp is greater than the target). * Converting the old schema_info table format to the new schema_migrations format, even when not all migration files from the old format have been applied, or the target version doesn't include all old style integer migration files. Currently, there's only integration tests available for the TimestampMigrator. I'll be adding real specs later, but they take more time to write. --- lib/sequel/extensions/migration.rb | 257 +++++++++++++++--- .../1273253849_create_sessions.rb | 9 + .../1273253851_create_nodes.rb | 9 + .../1273253853_3_create_users.rb | 3 + .../001_create_sessions.rb | 9 + .../002_create_nodes.rb | 9 + .../003_3_create_users.rb | 4 + .../1273253850_create_artists.rb | 9 + .../1273253852_create_albums.rb | 9 + .../1273253849_create_sessions.rb | 9 + .../1273253853_create_nodes.rb | 9 + .../1273253853_create_users.rb | 4 + .../1273253849_create_sessions.rb | 9 + .../1273253850_create_artists.rb | 9 + .../1273253851_create_nodes.rb | 9 + .../1273253852_create_albums.rb | 9 + .../1273253853_3_create_users.rb | 4 + .../1273253849_create_sessions.rb | 9 + .../1273253851_create_nodes.rb | 9 + .../1273253853_3_create_users.rb | 4 + spec/integration/migrator_test.rb | 136 ++++++++- 21 files changed, 493 insertions(+), 45 deletions(-) create mode 100644 spec/files/bad_timestamped_migrations/1273253849_create_sessions.rb create mode 100644 spec/files/bad_timestamped_migrations/1273253851_create_nodes.rb create mode 100644 spec/files/bad_timestamped_migrations/1273253853_3_create_users.rb create mode 100644 spec/files/convert_to_timestamp_migrations/001_create_sessions.rb create mode 100644 spec/files/convert_to_timestamp_migrations/002_create_nodes.rb create mode 100644 spec/files/convert_to_timestamp_migrations/003_3_create_users.rb create mode 100644 spec/files/convert_to_timestamp_migrations/1273253850_create_artists.rb create mode 100644 spec/files/convert_to_timestamp_migrations/1273253852_create_albums.rb create mode 100644 spec/files/duplicate_timestamped_migrations/1273253849_create_sessions.rb create mode 100644 spec/files/duplicate_timestamped_migrations/1273253853_create_nodes.rb create mode 100644 spec/files/duplicate_timestamped_migrations/1273253853_create_users.rb create mode 100644 spec/files/interleaved_timestamped_migrations/1273253849_create_sessions.rb create mode 100644 spec/files/interleaved_timestamped_migrations/1273253850_create_artists.rb create mode 100644 spec/files/interleaved_timestamped_migrations/1273253851_create_nodes.rb create mode 100644 spec/files/interleaved_timestamped_migrations/1273253852_create_albums.rb create mode 100644 spec/files/interleaved_timestamped_migrations/1273253853_3_create_users.rb create mode 100644 spec/files/timestamped_migrations/1273253849_create_sessions.rb create mode 100644 spec/files/timestamped_migrations/1273253851_create_nodes.rb create mode 100644 spec/files/timestamped_migrations/1273253853_3_create_users.rb diff --git a/lib/sequel/extensions/migration.rb b/lib/sequel/extensions/migration.rb index 53a4650454..226486487e 100644 --- a/lib/sequel/extensions/migration.rb +++ b/lib/sequel/extensions/migration.rb @@ -146,9 +146,9 @@ def self.migration(&block) MigrationDSL.create(&block) end - # The Migrator module performs migrations based on migration files in a + # The Migrator class performs migrations based on migration files in a # specified directory. The migration files should be named using the - # following pattern (in similar fashion to ActiveRecord migrations): + # following pattern: # # _.rb # @@ -156,10 +156,21 @@ def self.migration(&block) # # 001_create_sessions.rb # 002_add_data_column.rb - # ... + # + # You can also use timestamps as version numbers: + # + # 1273253850_create_sessions.rb + # 1273257248_add_data_column.rb # - # The migration files should contain one or more migration classes based - # on Sequel::Migration. + # If any migration filenames use timestamps as version numbers, Sequel + # uses the +TimestampMigrator+ to migrate, otherwise it uses the +IntegerMigrator+. + # The +TimestampMigrator+ can handle migrations that are run out of order + # as well as migrations with the same timestamp, + # while the +IntegerMigrator+ is more strict and raises exceptions for missing + # or duplicate migration files. + # + # The migration files should contain either one +Migration+ + # subclass or one <tt>Sequel.migration</tt> call. # # Migrations are generally run via the sequel command line tool, # using the -m and -M switches. The -m switch specifies the migration @@ -168,10 +179,11 @@ def self.migration(&block) # You can apply migrations using the Migrator API, as well (this is necessary # if you want to specify the version from which to migrate in addition to the version # to which to migrate). - # To apply a migration, the +apply+ method must be invoked with the database + # To apply a migrator, the +apply+ method must be invoked with the database # instance, the directory of migration files and the target version. If # no current version is supplied, it is read from the database. The migrator - # automatically creates a schema_info table in the database to keep track + # automatically creates a table (schema_info for integer migrations and + # schema_migrations for timestamped migrations). in the database to keep track # of the current migration version. If no migration version is stored in the # database, the version is considered to be 0. If no target version is # specified, the database is migrated to the latest version available in the @@ -181,21 +193,28 @@ def self.migration(&block) # # Sequel::Migrator.apply(DB, '.') # + # For example, to migrate the database all the way down: + # + # Sequel::Migrator.apply(DB, '.', 0) + # + # For example, to migrate the database to version 4: + # + # Sequel::Migrator.apply(DB, '.', 4) + # # To migrate the database from version 1 to version 5: # # Sequel::Migrator.apply(DB, '.', 5, 1) class Migrator - DEFAULT_SCHEMA_COLUMN = :version - DEFAULT_SCHEMA_TABLE = :schema_info MIGRATION_FILE_PATTERN = /\A\d+_.+\.rb\z/.freeze MIGRATION_SPLITTER = '_'.freeze + MINIMUM_TIMESTAMP = 1104566400 - # Exception class raised when there is an error with the migration - # file structure. + # Exception class raised when there is an error with the migrator's + # file structure, database, or arguments. class Error < Sequel::Error end - # Wrapper for run, maintaining backwards API compatibility + # Wrapper for +run+, maintaining backwards API compatibility def self.apply(db, directory, target = nil, current = nil) run(db, directory, :target => target, :current => current) end @@ -213,53 +232,101 @@ def self.apply(db, directory, target = nil, current = nil) # Sequel::Migrator.run(DB, "app1/migrations", :column=> :app2_version) # Sequel::Migrator.run(DB, "app2/migrations", :column => :app2_version, :table=>:schema_info2) def self.run(db, directory, opts={}) - new(db, directory, opts).run + migrator_class(directory).new(db, directory, opts).run end - # The column to use to hold the migration version number (defaults to :version) + # Choose the Migrator subclass to use. Uses the TimestampMigrator + # if the version number appears to be a unix time integer for a year + # after 2005, otherwise uses the IntegerMigrator. + def self.migrator_class(directory) + Dir.new(directory).each do |file| + next unless MIGRATION_FILE_PATTERN.match(file) + return TimestampMigrator if file.split(MIGRATION_SPLITTER, 2).first.to_i > MINIMUM_TIMESTAMP + end + IntegerMigrator + end + private_class_method :migrator_class + + # The column to use to hold the migration version number for integer migrations or + # filename for timestamp migrations (defaults to :version for integer migrations and + # :filename for timestamp migrations) attr_reader :column - # The current version for this migrator - attr_reader :current - # The database related to this migrator attr_reader :db - # The direction of the migrator, either :up or :down - attr_reader :direction - # The directory for this migrator's files attr_reader :directory - # The schema info dataset for this migrator + # The dataset for this migrator, representing the +schema_info+ table for integer + # migrations and the +schema_migrations+ table for timestamp migrations attr_reader :ds # All migration files in this migrator's directory attr_reader :files - # The migrations used by this migrator - attr_reader :migrations - - # The table to use to hold the migration version number (defaults to :schema_info) + # The table to use to hold the applied migration data (defaults to :schema_info for + # integer migrations and :schema_migrations for timestamp migrations) attr_reader :table # The target version for this migrator attr_reader :target - # Set up all state for the migrator instance + # Setup the state for the migrator def initialize(db, directory, opts={}) + raise(Error, "Must supply a valid migration path") unless File.directory?(directory) @db = db @directory = directory @files = get_migration_files - @table = opts[:table] || DEFAULT_SCHEMA_TABLE - @column = opts[:column] || DEFAULT_SCHEMA_COLUMN - @ds = schema_info_dataset + @table = opts[:table] || self.class.const_get(:DEFAULT_SCHEMA_TABLE) + @column = opts[:column] || self.class.const_get(:DEFAULT_SCHEMA_COLUMN) + @ds = schema_dataset + end + + private + + # Remove all migration classes. Done by the migrator to ensure that + # the correct migration classes are picked up. + def remove_migration_classes + # Remove class definitions + Migration.descendants.each do |c| + Object.send(:remove_const, c.to_s) rescue nil + end + Migration.descendants.clear # remove any defined migration classes + end + + # Return the integer migration version based on the filename. + def migration_version_from_file(filename) + filename.split(MIGRATION_SPLITTER, 2).first.to_i + end + end + + # The default migrator, recommended in most cases. Uses a simple incrementing + # version number starting with 1, where missing or duplicate migration file + # versions are not allowed. + class IntegerMigrator < Migrator + DEFAULT_SCHEMA_COLUMN = :version + DEFAULT_SCHEMA_TABLE = :schema_info + + Error = Migrator::Error + + # The current version for this migrator + attr_reader :current + + # The direction of the migrator, either :up or :down + attr_reader :direction + + # The migrations used by this migrator + attr_reader :migrations + + # Set up all state for the migrator instance + def initialize(db, directory, opts={}) + super @target = opts[:target] || latest_migration_version @current = opts[:current] || current_migration_version @direction = current < target ? :up : :down @migrations = get_migrations - raise(Error, "Must supply a valid migration path") unless File.directory?(directory) raise(Error, "No current version available") unless current raise(Error, "No target version available") unless target end @@ -300,11 +367,7 @@ def get_migration_files # Returns a list of migration classes filtered for the migration range and # ordered according to the migration direction. def get_migrations - # Remove class definitions - Migration.descendants.each do |c| - Object.send(:remove_const, c.to_s) rescue nil - end - Migration.descendants.clear # remove any defined migration classes + remove_migration_classes # load migration files files[up? ? (current + 1)..target : (target + 1)..current].compact.each{|f| load(f)} @@ -320,20 +383,15 @@ def latest_migration_version l ? migration_version_from_file(File.basename(l)) : nil end - # Return the integer migration version based on the filename. - def migration_version_from_file(filename) - filename.split(MIGRATION_SPLITTER, 2).first.to_i - end - # Returns the dataset for the schema_info table. If no such table # exists, it is automatically created. - def schema_info_dataset + def schema_dataset c = column ds = db.from(table) if !db.table_exists?(table) - db.create_table(table){Integer c, :default=>0} + db.create_table(table){Integer c, :default=>0, :null=>false} elsif !ds.columns.include?(c) - db.alter_table(table){add_column c, Integer, :default=>0} + db.alter_table(table){add_column c, Integer, :default=>0, :null=>false} end ds.insert(c=>0) if ds.empty? raise(Error, "More than 1 row in migrator table") if ds.count > 1 @@ -357,4 +415,117 @@ def version_numbers up? ? ((current+1)..target).to_a : (target..(current - 1)).to_a.reverse end end + + # The migrator used if any migration file version appears to be a timestamp. + # Stores filenames of migration files, and can figure out which migrations + # have not been applied and apply them, even if earlier migrations are added + # after later migrations. If you plan to do that, the responsibility is on + # you to make sure the migrations don't conflict. + class TimestampMigrator < Migrator + DEFAULT_SCHEMA_COLUMN = :filename + DEFAULT_SCHEMA_TABLE = :schema_migrations + + Error = Migrator::Error + + # Array of strings of applied migration filenames + attr_reader :applied_migrations + + # Get tuples of migrations, filenames, and actions for each migration + attr_reader :migration_tuples + + # Set up all state for the migrator instance + def initialize(db, directory, opts={}) + super + @target = opts[:target] + @applied_migrations = get_applied_migrations + @migration_tuples = get_migration_tuples + end + + # Apply all migration tuples on the database + def run + migration_tuples.each do |m, f, direction| + db.transaction do + m.apply(db, direction) + direction == :up ? ds.insert(column=>f) : ds.filter(column=>f).delete + end + end + nil + end + + private + + # Convert the schema_info table to the new schema_migrations table format, + # using the version of the schema_info table and the current migration files. + def convert_from_schema_info + v = db[IntegerMigrator::DEFAULT_SCHEMA_TABLE].get(IntegerMigrator::DEFAULT_SCHEMA_COLUMN) + ds = db.from(table) + files.each do |path| + f = File.basename(path) + if migration_version_from_file(f) <= v + ds.insert(column=>f) + end + end + end + + # Returns filenames of all applied migrations + def get_applied_migrations + am = ds.select_order_map(column) + missing_migration_files = am - files.map{|f| File.basename(f)} + raise(Error, "Applied migration files not in file system: #{missing_migration_files.join(', ')}") if missing_migration_files.length > 0 + am + end + + # Returns any migration files found in the migrator's directory. + def get_migration_files + files = [] + Dir.new(directory).each do |file| + next unless MIGRATION_FILE_PATTERN.match(file) + files << File.join(directory, file) + end + files.sort + end + + # Returns tuples of migration, filename, and direction + def get_migration_tuples + remove_migration_classes + up_mts = [] + down_mts = [] + ms = Migration.descendants + files.each do |path| + f = File.basename(path) + if target + if migration_version_from_file(f) > target + if applied_migrations.include?(f) + load(path) + down_mts << [ms.last, f, :down] + end + elsif !applied_migrations.include?(f) + load(path) + up_mts << [ms.last, f, :up] + end + elsif !applied_migrations.include?(f) + load(path) + up_mts << [ms.last, f, :up] + end + end + up_mts + down_mts.reverse + end + + # Returns the dataset for the schema_migrations table. If no such table + # exists, it is automatically created. + def schema_dataset + c = column + ds = db.from(table) + if !db.table_exists?(table) + db.create_table(table){String c, :primary_key=>true} + if db.table_exists?(:schema_info) and vha = db[:schema_info].all and vha.length == 1 and + vha.first.keys == [:version] and vha.first.values.first.is_a?(Integer) + convert_from_schema_info + end + elsif !ds.columns.include?(c) + raise(Error, "Migrator table #{table} does not contain column #{c}") + end + ds + end + end end diff --git a/spec/files/bad_timestamped_migrations/1273253849_create_sessions.rb b/spec/files/bad_timestamped_migrations/1273253849_create_sessions.rb new file mode 100644 index 0000000000..dc40b3539e --- /dev/null +++ b/spec/files/bad_timestamped_migrations/1273253849_create_sessions.rb @@ -0,0 +1,9 @@ +class CreateSessions < Sequel::Migration + def up + create_table(:sm1111){Integer :smc1} + end + + def down + get(:asdfsadfas) + end +end diff --git a/spec/files/bad_timestamped_migrations/1273253851_create_nodes.rb b/spec/files/bad_timestamped_migrations/1273253851_create_nodes.rb new file mode 100644 index 0000000000..dd98f49381 --- /dev/null +++ b/spec/files/bad_timestamped_migrations/1273253851_create_nodes.rb @@ -0,0 +1,9 @@ +Class.new(Sequel::Migration) do + def up + create_table(:sm2222){Integer :smc2} + end + + def down + drop_table(:sm2222) + end +end diff --git a/spec/files/bad_timestamped_migrations/1273253853_3_create_users.rb b/spec/files/bad_timestamped_migrations/1273253853_3_create_users.rb new file mode 100644 index 0000000000..ea40a6244a --- /dev/null +++ b/spec/files/bad_timestamped_migrations/1273253853_3_create_users.rb @@ -0,0 +1,3 @@ +Sequel.migration do + up{get(:asdfsadfas)} +end diff --git a/spec/files/convert_to_timestamp_migrations/001_create_sessions.rb b/spec/files/convert_to_timestamp_migrations/001_create_sessions.rb new file mode 100644 index 0000000000..c9b03cf469 --- /dev/null +++ b/spec/files/convert_to_timestamp_migrations/001_create_sessions.rb @@ -0,0 +1,9 @@ +class CreateSessions < Sequel::Migration + def up + create_table(:sm1111){Integer :smc1} + end + + def down + drop_table(:sm1111) + end +end diff --git a/spec/files/convert_to_timestamp_migrations/002_create_nodes.rb b/spec/files/convert_to_timestamp_migrations/002_create_nodes.rb new file mode 100644 index 0000000000..dd98f49381 --- /dev/null +++ b/spec/files/convert_to_timestamp_migrations/002_create_nodes.rb @@ -0,0 +1,9 @@ +Class.new(Sequel::Migration) do + def up + create_table(:sm2222){Integer :smc2} + end + + def down + drop_table(:sm2222) + end +end diff --git a/spec/files/convert_to_timestamp_migrations/003_3_create_users.rb b/spec/files/convert_to_timestamp_migrations/003_3_create_users.rb new file mode 100644 index 0000000000..dcd3aaf636 --- /dev/null +++ b/spec/files/convert_to_timestamp_migrations/003_3_create_users.rb @@ -0,0 +1,4 @@ +Sequel.migration do + up{create_table(:sm3333){Integer :smc3}} + down{drop_table(:sm3333)} +end diff --git a/spec/files/convert_to_timestamp_migrations/1273253850_create_artists.rb b/spec/files/convert_to_timestamp_migrations/1273253850_create_artists.rb new file mode 100644 index 0000000000..6068c6e721 --- /dev/null +++ b/spec/files/convert_to_timestamp_migrations/1273253850_create_artists.rb @@ -0,0 +1,9 @@ +class CreateArtists < Sequel::Migration + def up + create_table(:sm1122){Integer :smc12} + end + + def down + drop_table(:sm1122) + end +end diff --git a/spec/files/convert_to_timestamp_migrations/1273253852_create_albums.rb b/spec/files/convert_to_timestamp_migrations/1273253852_create_albums.rb new file mode 100644 index 0000000000..c8427a28c5 --- /dev/null +++ b/spec/files/convert_to_timestamp_migrations/1273253852_create_albums.rb @@ -0,0 +1,9 @@ +class CreateAlbums < Sequel::Migration + def up + create_table(:sm2233){Integer :smc23} + end + + def down + drop_table(:sm2233) + end +end diff --git a/spec/files/duplicate_timestamped_migrations/1273253849_create_sessions.rb b/spec/files/duplicate_timestamped_migrations/1273253849_create_sessions.rb new file mode 100644 index 0000000000..c9b03cf469 --- /dev/null +++ b/spec/files/duplicate_timestamped_migrations/1273253849_create_sessions.rb @@ -0,0 +1,9 @@ +class CreateSessions < Sequel::Migration + def up + create_table(:sm1111){Integer :smc1} + end + + def down + drop_table(:sm1111) + end +end diff --git a/spec/files/duplicate_timestamped_migrations/1273253853_create_nodes.rb b/spec/files/duplicate_timestamped_migrations/1273253853_create_nodes.rb new file mode 100644 index 0000000000..dd98f49381 --- /dev/null +++ b/spec/files/duplicate_timestamped_migrations/1273253853_create_nodes.rb @@ -0,0 +1,9 @@ +Class.new(Sequel::Migration) do + def up + create_table(:sm2222){Integer :smc2} + end + + def down + drop_table(:sm2222) + end +end diff --git a/spec/files/duplicate_timestamped_migrations/1273253853_create_users.rb b/spec/files/duplicate_timestamped_migrations/1273253853_create_users.rb new file mode 100644 index 0000000000..dcd3aaf636 --- /dev/null +++ b/spec/files/duplicate_timestamped_migrations/1273253853_create_users.rb @@ -0,0 +1,4 @@ +Sequel.migration do + up{create_table(:sm3333){Integer :smc3}} + down{drop_table(:sm3333)} +end diff --git a/spec/files/interleaved_timestamped_migrations/1273253849_create_sessions.rb b/spec/files/interleaved_timestamped_migrations/1273253849_create_sessions.rb new file mode 100644 index 0000000000..c9b03cf469 --- /dev/null +++ b/spec/files/interleaved_timestamped_migrations/1273253849_create_sessions.rb @@ -0,0 +1,9 @@ +class CreateSessions < Sequel::Migration + def up + create_table(:sm1111){Integer :smc1} + end + + def down + drop_table(:sm1111) + end +end diff --git a/spec/files/interleaved_timestamped_migrations/1273253850_create_artists.rb b/spec/files/interleaved_timestamped_migrations/1273253850_create_artists.rb new file mode 100644 index 0000000000..6068c6e721 --- /dev/null +++ b/spec/files/interleaved_timestamped_migrations/1273253850_create_artists.rb @@ -0,0 +1,9 @@ +class CreateArtists < Sequel::Migration + def up + create_table(:sm1122){Integer :smc12} + end + + def down + drop_table(:sm1122) + end +end diff --git a/spec/files/interleaved_timestamped_migrations/1273253851_create_nodes.rb b/spec/files/interleaved_timestamped_migrations/1273253851_create_nodes.rb new file mode 100644 index 0000000000..dd98f49381 --- /dev/null +++ b/spec/files/interleaved_timestamped_migrations/1273253851_create_nodes.rb @@ -0,0 +1,9 @@ +Class.new(Sequel::Migration) do + def up + create_table(:sm2222){Integer :smc2} + end + + def down + drop_table(:sm2222) + end +end diff --git a/spec/files/interleaved_timestamped_migrations/1273253852_create_albums.rb b/spec/files/interleaved_timestamped_migrations/1273253852_create_albums.rb new file mode 100644 index 0000000000..c8427a28c5 --- /dev/null +++ b/spec/files/interleaved_timestamped_migrations/1273253852_create_albums.rb @@ -0,0 +1,9 @@ +class CreateAlbums < Sequel::Migration + def up + create_table(:sm2233){Integer :smc23} + end + + def down + drop_table(:sm2233) + end +end diff --git a/spec/files/interleaved_timestamped_migrations/1273253853_3_create_users.rb b/spec/files/interleaved_timestamped_migrations/1273253853_3_create_users.rb new file mode 100644 index 0000000000..dcd3aaf636 --- /dev/null +++ b/spec/files/interleaved_timestamped_migrations/1273253853_3_create_users.rb @@ -0,0 +1,4 @@ +Sequel.migration do + up{create_table(:sm3333){Integer :smc3}} + down{drop_table(:sm3333)} +end diff --git a/spec/files/timestamped_migrations/1273253849_create_sessions.rb b/spec/files/timestamped_migrations/1273253849_create_sessions.rb new file mode 100644 index 0000000000..c9b03cf469 --- /dev/null +++ b/spec/files/timestamped_migrations/1273253849_create_sessions.rb @@ -0,0 +1,9 @@ +class CreateSessions < Sequel::Migration + def up + create_table(:sm1111){Integer :smc1} + end + + def down + drop_table(:sm1111) + end +end diff --git a/spec/files/timestamped_migrations/1273253851_create_nodes.rb b/spec/files/timestamped_migrations/1273253851_create_nodes.rb new file mode 100644 index 0000000000..dd98f49381 --- /dev/null +++ b/spec/files/timestamped_migrations/1273253851_create_nodes.rb @@ -0,0 +1,9 @@ +Class.new(Sequel::Migration) do + def up + create_table(:sm2222){Integer :smc2} + end + + def down + drop_table(:sm2222) + end +end diff --git a/spec/files/timestamped_migrations/1273253853_3_create_users.rb b/spec/files/timestamped_migrations/1273253853_3_create_users.rb new file mode 100644 index 0000000000..dcd3aaf636 --- /dev/null +++ b/spec/files/timestamped_migrations/1273253853_3_create_users.rb @@ -0,0 +1,4 @@ +Sequel.migration do + up{create_table(:sm3333){Integer :smc3}} + down{drop_table(:sm3333)} +end diff --git a/spec/integration/migrator_test.rb b/spec/integration/migrator_test.rb index fcbb3fd65b..d6c322321f 100644 --- a/spec/integration/migrator_test.rb +++ b/spec/integration/migrator_test.rb @@ -5,13 +5,13 @@ before do @db = INTEGRATION_DB @m = Sequel::Migrator - @dir = 'spec/files/integer_migrations' end after do - [:schema_info, :sm1111, :sm2222, :sm3333].each{|n| @db.drop_table(n) rescue nil} + [:schema_info, :schema_migrations, :sm1111, :sm1122, :sm2222, :sm2233, :sm3333, :sm11111, :sm22222].each{|n| @db.drop_table(n) rescue nil} end specify "should be able to migrate up and down all the way successfully" do + @dir = 'spec/files/integer_migrations' @m.apply(@db, @dir) [:schema_info, :sm1111, :sm2222, :sm3333].each{|n| @db.table_exists?(n).should be_true} @db[:schema_info].get(:version).should == 3 @@ -21,6 +21,7 @@ end specify "should be able to migrate up and down to specific versions successfully" do + @dir = 'spec/files/integer_migrations' @m.apply(@db, @dir, 2) [:schema_info, :sm1111, :sm2222].each{|n| @db.table_exists?(n).should be_true} @db.table_exists?(:sm3333).should be_false @@ -52,4 +53,135 @@ @db.table_exists?(:sm11111).should be_true @db[:schema_info].get(:version).should == 1 end + + specify "should handle migrating up or down all the way with timestamped migrations" do + @dir = 'spec/files/timestamped_migrations' + @m.apply(@db, @dir) + [:schema_migrations, :sm1111, :sm2222, :sm3333].each{|n| @db.table_exists?(n).should be_true} + @db[:schema_migrations].select_order_map(:filename).should == %w'1273253849_create_sessions.rb 1273253851_create_nodes.rb 1273253853_3_create_users.rb' + @m.apply(@db, @dir, 0) + [:sm1111, :sm2222, :sm3333].each{|n| @db.table_exists?(n).should be_false} + @db[:schema_migrations].select_order_map(:filename).should == [] + end + + specify "should handle migrating up or down to specific timestamps with timestamped migrations" do + @dir = 'spec/files/timestamped_migrations' + @m.apply(@db, @dir, 1273253851) + [:schema_migrations, :sm1111, :sm2222].each{|n| @db.table_exists?(n).should be_true} + @db.table_exists?(:sm3333).should be_false + @db[:schema_migrations].select_order_map(:filename).should == %w'1273253849_create_sessions.rb 1273253851_create_nodes.rb' + @m.apply(@db, @dir, 1273253849) + [:sm2222, :sm3333].each{|n| @db.table_exists?(n).should be_false} + @db.table_exists?(:sm1111).should be_true + @db[:schema_migrations].select_order_map(:filename).should == %w'1273253849_create_sessions.rb' + end + + specify "should apply all missing files when migrating up with timestamped migrations" do + @dir = 'spec/files/timestamped_migrations' + @m.apply(@db, @dir) + @dir = 'spec/files/interleaved_timestamped_migrations' + @m.apply(@db, @dir) + [:schema_migrations, :sm1111, :sm1122, :sm2222, :sm2233, :sm3333].each{|n| @db.table_exists?(n).should be_true} + @db[:schema_migrations].select_order_map(:filename).should == %w'1273253849_create_sessions.rb 1273253850_create_artists.rb 1273253851_create_nodes.rb 1273253852_create_albums.rb 1273253853_3_create_users.rb' + end + + specify "should not apply down action to migrations where up action hasn't been applied" do + @dir = 'spec/files/timestamped_migrations' + @m.apply(@db, @dir) + @dir = 'spec/files/interleaved_timestamped_migrations' + @m.apply(@db, @dir, 0) + [:sm1111, :sm1122, :sm2222, :sm2233, :sm3333].each{|n| @db.table_exists?(n).should be_false} + @db[:schema_migrations].select_order_map(:filename).should == [] + end + + specify "should handle updating to a specific timestamp when interleaving migrations with timestamps" do + @dir = 'spec/files/timestamped_migrations' + @m.apply(@db, @dir) + @dir = 'spec/files/interleaved_timestamped_migrations' + @m.apply(@db, @dir, 1273253851) + [:schema_migrations, :sm1111, :sm1122, :sm2222].each{|n| @db.table_exists?(n).should be_true} + [:sm2233, :sm3333].each{|n| @db.table_exists?(n).should be_false} + @db[:schema_migrations].select_order_map(:filename).should == %w'1273253849_create_sessions.rb 1273253850_create_artists.rb 1273253851_create_nodes.rb' + end + + specify "should correctly update schema_migrations table when an error occurs when migrating up or down using timestamped migrations" do + @dir = 'spec/files/bad_timestamped_migrations' + proc{@m.apply(@db, @dir)}.should raise_error + [:schema_migrations, :sm1111, :sm2222].each{|n| @db.table_exists?(n).should be_true} + @db.table_exists?(:sm3333).should be_false + @db[:schema_migrations].select_order_map(:filename).should == %w'1273253849_create_sessions.rb 1273253851_create_nodes.rb' + proc{@m.apply(@db, @dir, 0)}.should raise_error + [:sm2222, :sm3333].each{|n| @db.table_exists?(n).should be_false} + @db.table_exists?(:sm1111).should be_true + @db[:schema_migrations].select_order_map(:filename).should == %w'1273253849_create_sessions.rb' + end + + specify "should handle multiple migrations with the same timestamp correctly" do + @dir = 'spec/files/duplicate_timestamped_migrations' + @m.apply(@db, @dir) + [:schema_migrations, :sm1111, :sm2222, :sm3333].each{|n| @db.table_exists?(n).should be_true} + @db[:schema_migrations].select_order_map(:filename).should == %w'1273253849_create_sessions.rb 1273253853_create_nodes.rb 1273253853_create_users.rb' + @m.apply(@db, @dir, 1273253853) + [:sm1111, :sm2222, :sm3333].each{|n| @db.table_exists?(n).should be_true} + @db[:schema_migrations].select_order_map(:filename).should == %w'1273253849_create_sessions.rb 1273253853_create_nodes.rb 1273253853_create_users.rb' + @m.apply(@db, @dir, 1273253849) + [:sm1111].each{|n| @db.table_exists?(n).should be_true} + [:sm2222, :sm3333].each{|n| @db.table_exists?(n).should be_false} + @db[:schema_migrations].select_order_map(:filename).should == %w'1273253849_create_sessions.rb' + @m.apply(@db, @dir, 1273253848) + [:sm1111, :sm2222, :sm3333].each{|n| @db.table_exists?(n).should be_false} + @db[:schema_migrations].select_order_map(:filename).should == [] + end + + specify "should convert schema_info table to schema_migrations table" do + @dir = 'spec/files/integer_migrations' + @m.apply(@db, @dir) + [:schema_info, :sm1111, :sm2222, :sm3333].each{|n| @db.table_exists?(n).should be_true} + [:schema_migrations, :sm1122, :sm2233].each{|n| @db.table_exists?(n).should be_false} + + @dir = 'spec/files/convert_to_timestamp_migrations' + @m.apply(@db, @dir) + [:schema_info, :sm1111, :sm2222, :sm3333, :schema_migrations, :sm1122, :sm2233].each{|n| @db.table_exists?(n).should be_true} + @db[:schema_migrations].select_order_map(:filename).should == %w'001_create_sessions.rb 002_create_nodes.rb 003_3_create_users.rb 1273253850_create_artists.rb 1273253852_create_albums.rb' + + @m.apply(@db, @dir, 4) + [:schema_info, :schema_migrations, :sm1111, :sm2222, :sm3333].each{|n| @db.table_exists?(n).should be_true} + [:sm1122, :sm2233].each{|n| @db.table_exists?(n).should be_false} + @db[:schema_migrations].select_order_map(:filename).should == %w'001_create_sessions.rb 002_create_nodes.rb 003_3_create_users.rb' + + @m.apply(@db, @dir, 0) + [:schema_info, :schema_migrations].each{|n| @db.table_exists?(n).should be_true} + [:sm1111, :sm2222, :sm3333, :sm1122, :sm2233].each{|n| @db.table_exists?(n).should be_false} + @db[:schema_migrations].select_order_map(:filename).should == [] + end + + specify "should handle unapplied migrations when migrating schema_info table to schema_migrations table" do + @dir = 'spec/files/integer_migrations' + @m.apply(@db, @dir, 2) + [:schema_info, :sm1111, :sm2222].each{|n| @db.table_exists?(n).should be_true} + [:schema_migrations, :sm3333, :sm1122, :sm2233].each{|n| @db.table_exists?(n).should be_false} + + @dir = 'spec/files/convert_to_timestamp_migrations' + @m.apply(@db, @dir, 1273253850) + [:schema_info, :sm1111, :sm2222, :sm3333, :schema_migrations, :sm1122].each{|n| @db.table_exists?(n).should be_true} + [:sm2233].each{|n| @db.table_exists?(n).should be_false} + @db[:schema_migrations].select_order_map(:filename).should == %w'001_create_sessions.rb 002_create_nodes.rb 003_3_create_users.rb 1273253850_create_artists.rb' + end + + specify "should handle unapplied migrations when migrating schema_info table to schema_migrations table and target is less than last integer migration version" do + @dir = 'spec/files/integer_migrations' + @m.apply(@db, @dir, 1) + [:schema_info, :sm1111].each{|n| @db.table_exists?(n).should be_true} + [:schema_migrations, :sm2222, :sm3333, :sm1122, :sm2233].each{|n| @db.table_exists?(n).should be_false} + + @dir = 'spec/files/convert_to_timestamp_migrations' + @m.apply(@db, @dir, 2) + [:schema_info, :sm1111, :sm2222, :schema_migrations].each{|n| @db.table_exists?(n).should be_true} + [:sm3333, :sm1122, :sm2233].each{|n| @db.table_exists?(n).should be_false} + @db[:schema_migrations].select_order_map(:filename).should == %w'001_create_sessions.rb 002_create_nodes.rb' + + @m.apply(@db, @dir) + [:schema_info, :sm1111, :sm2222, :schema_migrations, :sm3333, :sm1122, :sm2233].each{|n| @db.table_exists?(n).should be_true} + @db[:schema_migrations].select_order_map(:filename).should == %w'001_create_sessions.rb 002_create_nodes.rb 003_3_create_users.rb 1273253850_create_artists.rb 1273253852_create_albums.rb' + end end