exodus 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +4 -0
- data/.rvmrc +1 -0
- data/.travis.yml +14 -0
- data/CHANGELOG.md +7 -0
- data/README.md +3 -3
- data/exodus.gemspec +1 -1
- data/lib/exodus.rb +31 -16
- data/lib/exodus/config/migration_info.rb +59 -0
- data/lib/exodus/migrations/migration.rb +165 -0
- data/lib/exodus/migrations/migration_error.rb +11 -0
- data/lib/exodus/migrations/migration_status.rb +39 -0
- data/lib/exodus/version.rb +1 -1
- data/spec/exodus/migration_spec.rb +187 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/support/config.yml +6 -0
- data/spec/support/user_support.rb +4 -0
- data/tasks/exodus.rake +78 -0
- metadata +21 -4
data/.rspec
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use ruby-1.9.3-p194@exodus
|
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/README.md
CHANGED
@@ -3,15 +3,15 @@ Exodus - a migration framework for MongoDb
|
|
3
3
|
|
4
4
|
# Intro
|
5
5
|
|
6
|
-
## A migration Framework for a schemaless database
|
6
|
+
## A migration Framework for a schemaless database ??
|
7
7
|
|
8
|
-
After working with Mongo for long time now I can tell you
|
8
|
+
After working with Mongo for long time now I can tell you working with a schemaless database does not mean you will never need any migrations. Within the same collection Mongo allows to have documents with a complete different structure, however in some case is you might want to keep data consistency; Especially when your code is live in production and used by millions of users.
|
9
9
|
|
10
10
|
There is a plenty of way to modify documents data structure and after a deep reflexion I realized it makes more sens to use migration framework. A migration framework provides a lot of advantages, such as:
|
11
11
|
|
12
12
|
* It allows you to know at any time which migration has been ran on any given system
|
13
13
|
* It's Auto runnable on deploy
|
14
|
-
|
14
|
+
* When switching enviromment (dev, pre-prod, prod) you don't need to worry if the script has been ran or not. The framework takes care of it for you
|
15
15
|
|
16
16
|
|
17
17
|
# Installation
|
data/exodus.gemspec
CHANGED
@@ -4,7 +4,7 @@ require File.expand_path('../lib/exodus/version', __FILE__)
|
|
4
4
|
Gem::Specification.new do |gem|
|
5
5
|
gem.authors = ['Thomas Dmytryk']
|
6
6
|
gem.email = ['thomas@fanhattan.com', 'thomas.dmytryk@supinfo.com']
|
7
|
-
gem.description = %q{Exodus is a migration framework for
|
7
|
+
gem.description = %q{Exodus is a migration framework for MongoDb}
|
8
8
|
gem.summary = %q{Exodus uses mongomapper to provide a complete migration framework}
|
9
9
|
gem.homepage = ''
|
10
10
|
gem.license = 'MIT'
|
data/lib/exodus.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
require 'mongo_mapper'
|
2
|
-
|
2
|
+
require File.dirname(__FILE__) + '/exodus/config/migration_info'
|
3
|
+
require File.dirname(__FILE__) + '/exodus/migrations/migration'
|
4
|
+
require File.dirname(__FILE__) + '/exodus/migrations/migration_error'
|
5
|
+
require File.dirname(__FILE__) + '/exodus/migrations/migration_status'
|
3
6
|
|
4
7
|
module Exodus
|
5
8
|
class << self
|
@@ -13,39 +16,43 @@ module Exodus
|
|
13
16
|
yield(configuration) if block_given?
|
14
17
|
end
|
15
18
|
|
16
|
-
#
|
19
|
+
# Executes a number of migrations equal to step (or all of them if step is nil)
|
17
20
|
def run_migrations(direction, migrations, step = nil)
|
18
21
|
if migrations
|
19
|
-
sorted_migrations = sort_migrations(migrations)
|
20
22
|
sorted_migrations = order_with_direction(sorted_migrations, direction)
|
21
23
|
sorted_migrations = sorted_migrations.shift(step.to_i) if step
|
22
24
|
|
23
|
-
|
25
|
+
run_each(sorted_migrations)
|
24
26
|
else
|
25
27
|
puts "no migrations given in argument!"
|
26
28
|
end
|
27
29
|
end
|
28
30
|
|
29
|
-
|
30
|
-
|
31
|
+
# Migrations order need to be reverted if the direction is down
|
32
|
+
# (we want the latest executed migration to be the first reverted)
|
33
|
+
def order_with_direction(migrations, direction)
|
34
|
+
sorted_migrations = sort_migrations(migrations)
|
35
|
+
direction == Migration::UP ? sorted_migrations : sorted_migrations.reverse
|
31
36
|
end
|
32
37
|
|
33
|
-
def
|
34
|
-
|
38
|
+
def sort_migrations(migrations)
|
39
|
+
migrations.sort_by {|migration,args| migration.migration_number }
|
35
40
|
end
|
36
41
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
puts "\n"
|
42
|
+
# Runs each migration separately, migration's arguments default value is set to an empty hash
|
43
|
+
def run_each(direction, migrations)
|
44
|
+
migrations.each do |migration_class, args|
|
45
|
+
print_tabulation { run_one_migration(migration_class, direction, args || {}) }
|
46
|
+
end
|
43
47
|
end
|
44
48
|
|
49
|
+
# Looks up in the database if a migration with the same class and same arguments already exists
|
50
|
+
# Otherwise instanciate a new one
|
51
|
+
# Runs the migration if it is runnable
|
45
52
|
def run_one_migration(migration_class, direction, args)
|
46
53
|
# Going throught MRD because MM request returns nil for some reason
|
47
54
|
current_migration = migration_class.load(migration_class.collection.find('status.arguments' => args).first)
|
48
|
-
current_migration ||= migration_class.new(status
|
55
|
+
current_migration ||= migration_class.new(:status => {:arguments => args})
|
49
56
|
|
50
57
|
if current_migration.is_runnable?(direction)
|
51
58
|
# Make sure we save all info in case of a failure
|
@@ -58,11 +65,19 @@ module Exodus
|
|
58
65
|
raise
|
59
66
|
end
|
60
67
|
|
61
|
-
# save the migration
|
62
68
|
current_migration.save!
|
63
69
|
else
|
64
70
|
puts "#{current_migration.class}#{current_migration.status.arguments}(#{direction}) as Already been run (or is not runnable)."
|
65
71
|
end
|
66
72
|
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# Prints tabulation before execting a given block
|
77
|
+
def print_tabulation
|
78
|
+
puts "\n"
|
79
|
+
yield if block_given?
|
80
|
+
puts "\n"
|
81
|
+
end
|
67
82
|
end
|
68
83
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Exodus
|
2
|
+
class MigrationInfo
|
3
|
+
attr_accessor :info
|
4
|
+
attr_reader :config_file, :db, :connection
|
5
|
+
|
6
|
+
def initialize(file = nil)
|
7
|
+
config_file = file if file
|
8
|
+
end
|
9
|
+
|
10
|
+
def db=(database)
|
11
|
+
MongoMapper.database = database
|
12
|
+
end
|
13
|
+
|
14
|
+
def connection=(conn)
|
15
|
+
MongoMapper.connection = conn
|
16
|
+
end
|
17
|
+
|
18
|
+
def config_file=(file)
|
19
|
+
if File.exists?(file)
|
20
|
+
@config_file = file
|
21
|
+
@info = YAML.load_file(file)
|
22
|
+
else
|
23
|
+
raise ArgumentError, "#{file} not found"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def migrate
|
28
|
+
verify_yml_syntax { @info['migration']['migrate'] }
|
29
|
+
end
|
30
|
+
|
31
|
+
def rollback
|
32
|
+
verify_yml_syntax { @info['migration']['rollback'] }
|
33
|
+
end
|
34
|
+
|
35
|
+
def migrate_custom
|
36
|
+
verify_yml_syntax { @info['migration']['custom']['migrate'] }
|
37
|
+
end
|
38
|
+
|
39
|
+
def rollback_custom
|
40
|
+
verify_yml_syntax { @info['migration']['custom']['rollback'] }
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_s
|
44
|
+
@info
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def verify_yml_syntax
|
50
|
+
Raise StandardError, "No configuration file specified" unless self.config_file
|
51
|
+
|
52
|
+
begin
|
53
|
+
yield if block_given?
|
54
|
+
rescue
|
55
|
+
Raise StandardError, "Syntax error detected in config file #{self.config_file}. To find the good syntax take a look at the documentation."
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
module Exodus
|
2
|
+
class Migration
|
3
|
+
include MongoMapper::Document
|
4
|
+
UP = 'up'
|
5
|
+
DOWN = 'down'
|
6
|
+
@migrations_with_args = []
|
7
|
+
|
8
|
+
timestamps!
|
9
|
+
|
10
|
+
key :description, String
|
11
|
+
key :status_complete, Integer, :default => 1
|
12
|
+
key :rerunnable_safe, Boolean, :default => false # Be careful if the job is rerunnable_safe he will re-run on each db:migrate
|
13
|
+
|
14
|
+
has_one :status, :class_name => "Exodus::MigrationStatus", :autosave => true
|
15
|
+
|
16
|
+
class << self
|
17
|
+
attr_accessor :migration_number
|
18
|
+
|
19
|
+
# Overides #inherited to have an easy and reliable way to find all migrations
|
20
|
+
# Migrations need to have embedded callbacks on depending on the MM's version
|
21
|
+
def inherited(klass)
|
22
|
+
klass.embedded_callbacks_on if defined?(MongoMapper::Plugins::EmbeddedCallbacks::ClassMethods) #MongoMapper version compatibility
|
23
|
+
klass.migration_number = 0
|
24
|
+
@migrations_with_args << [klass]
|
25
|
+
super(klass)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Using a list of migrations
|
29
|
+
# Formats and overrides migrations without arguments using ones that have given arguments
|
30
|
+
# Removes duplicates
|
31
|
+
# migrations: list of migrations => [[MyMigration, {:my_args => 'some_args'}]]
|
32
|
+
def load_all(migrations)
|
33
|
+
if migrations
|
34
|
+
migrations.each do |migration, args|
|
35
|
+
if migration && args
|
36
|
+
formated_migration = format(migration, args)
|
37
|
+
migration, args = formated_migration
|
38
|
+
|
39
|
+
unless @migrations_with_args.include?(formated_migration)
|
40
|
+
@migrations_with_args.delete_if {|loaded_migration, loaded_args| migration == loaded_migration && (loaded_args.nil? || loaded_args.empty?) }
|
41
|
+
@migrations_with_args << formated_migration
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
@migrations_with_args
|
48
|
+
end
|
49
|
+
|
50
|
+
# Using a list of migrations formats them and removes duplicates
|
51
|
+
# migrations: list of migrations => [[MyMigration, {:my_args => 'some_args'}]]
|
52
|
+
def load_custom(migrations)
|
53
|
+
migrations.map {|migration_str, args| format(migration_str, args) }.uniq
|
54
|
+
end
|
55
|
+
|
56
|
+
# Formats a given migration making sure the first argument is a class
|
57
|
+
# and the second one -if it exists- is a none empty hash
|
58
|
+
def format(migration, args = {})
|
59
|
+
migration_klass = migration.is_a?(String) ? migration.constantize : migration
|
60
|
+
args.is_a?(Hash) && args.empty? ? [migration_klass] : [migration_klass, args]
|
61
|
+
end
|
62
|
+
|
63
|
+
# Prints in the console all migrations class with their name and description
|
64
|
+
def list
|
65
|
+
puts "\n Migration n#: \t\t Name: \t\t\t\t Description:"
|
66
|
+
puts '-' * 100, "\n"
|
67
|
+
|
68
|
+
@migrations_with_args.map do|migration, args|
|
69
|
+
m = migration.new
|
70
|
+
puts "\t#{migration.migration_number} \t\t #{migration.name} \t\t #{m.description}"
|
71
|
+
end
|
72
|
+
|
73
|
+
puts "\n\n"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Prints in the console all migrations that has been ran at least once with their name and description
|
77
|
+
def db_status
|
78
|
+
puts "\n Migration n#: \t Name: \t\t Direction: Arguments: Current Status: \t Last completion Date: \t\t Current Message:"
|
79
|
+
puts '-' * 175, "\n"
|
80
|
+
|
81
|
+
Migration.all.each do|migration|
|
82
|
+
puts "\t#{migration.class.migration_number} \t #{migration.class.name} \t #{migration.status.to_string}"
|
83
|
+
end
|
84
|
+
|
85
|
+
puts "\n\n"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Makes sure status get instanciated on migration's instanciation
|
90
|
+
def initialize(args = {})
|
91
|
+
self.build_status(args[:status])
|
92
|
+
super(args)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Runs the migration following the direction
|
96
|
+
# sets the status, the execution time and the last succesful_completion date
|
97
|
+
def run(direction)
|
98
|
+
self.status.direction = direction
|
99
|
+
|
100
|
+
# reset the status if the job is rerunnable and has already be completed
|
101
|
+
self.status = self.status.reset if self.rerunnable_safe && completed?(direction)
|
102
|
+
self.status.execution_time = time_it { self.send(direction) }
|
103
|
+
self.status.last_succesful_completion = Time.now
|
104
|
+
end
|
105
|
+
|
106
|
+
# Sets an error to migration status
|
107
|
+
def failure=(exception)
|
108
|
+
self.status.error = MigrationError.new(:error_message => exception.message, :error_class => exception.class, :error_backtrace => exception.backtrace)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Checks if a migration can be run
|
112
|
+
def is_runnable?(direction)
|
113
|
+
rerunnable_safe || (direction == UP && status.current_status < status_complete) || (direction == DOWN && status.current_status > 0)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Checks if a migration as been completed
|
117
|
+
def completed?(direction)
|
118
|
+
return false if self.status.execution_time == 0
|
119
|
+
(direction == UP && self.status.current_status == self.status_complete) || (direction == DOWN && self.status.current_status == 0)
|
120
|
+
end
|
121
|
+
|
122
|
+
protected
|
123
|
+
|
124
|
+
# Executes a given block if the status has not being processed
|
125
|
+
# Then update the status
|
126
|
+
def step(step_message = nil, step_status = 1)
|
127
|
+
unless status.status_processed?(status.direction, step_status)
|
128
|
+
self.status.message = step_message
|
129
|
+
puts "\t #{step_message}"
|
130
|
+
|
131
|
+
yield if block_given?
|
132
|
+
self.status.current_status += status.direction_to_i
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Prints a given message with the current time
|
137
|
+
def tick(msg)
|
138
|
+
puts "#{Time.now}: #{msg}"
|
139
|
+
end
|
140
|
+
|
141
|
+
# Executes a block and returns the time it took to be executed
|
142
|
+
def time_it
|
143
|
+
puts "Running #{self.class}[#{self.status.arguments}](#{self.status.direction})"
|
144
|
+
|
145
|
+
start = Time.now
|
146
|
+
yield if block_given?
|
147
|
+
end_time = Time.now - start
|
148
|
+
|
149
|
+
puts "Tasks #{self.class} executed in #{end_time} seconds. \n\n"
|
150
|
+
end_time
|
151
|
+
end
|
152
|
+
|
153
|
+
# contains the code that will be executed when run(up) will be called
|
154
|
+
def up
|
155
|
+
raise StandardError, 'Needs to be implemented in child class.'
|
156
|
+
end
|
157
|
+
|
158
|
+
# contains the code that will be executed when run(down) will be called
|
159
|
+
def down
|
160
|
+
raise StandardError, 'Needs to be implemented in child class.'
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
Dir[File.dirname(__FILE__) + "/*.rb"].sort.each { |file| require file;}
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Exodus
|
2
|
+
class MigrationStatus
|
3
|
+
include MongoMapper::EmbeddedDocument
|
4
|
+
|
5
|
+
key :message, String
|
6
|
+
key :current_status, Integer, :default => 0
|
7
|
+
key :execution_time, Float, :default => 0
|
8
|
+
key :last_succesful_completion, Time
|
9
|
+
key :direction, String, :default => Migration::UP
|
10
|
+
key :arguments, Hash, :default => {}
|
11
|
+
|
12
|
+
embedded_in :migration
|
13
|
+
has_one :error, :class_name => "Exodus::MigrationError", :autosave => true
|
14
|
+
|
15
|
+
def direction_to_i
|
16
|
+
self.direction == Migration::UP ? 1 : -1
|
17
|
+
end
|
18
|
+
|
19
|
+
# Checks if a status has been processed
|
20
|
+
# a Status has been processed when:
|
21
|
+
# The current status is superior or equal to the given status and the migration direction is UP
|
22
|
+
# The current status is inferior or equal to the given status and the migration direction is DOWN
|
23
|
+
def status_processed?(migration_direction, status_to_process)
|
24
|
+
(migration_direction == Migration::UP && current_status >= status_to_process) || (migration_direction == Migration::DOWN && current_status <= status_to_process)
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_string
|
28
|
+
"\t#{direction}\t\t #{arguments}\t\t #{current_status} \t\t #{last_succesful_completion} \t\t #{message}"
|
29
|
+
end
|
30
|
+
|
31
|
+
# Resets a status
|
32
|
+
def reset
|
33
|
+
self.message = nil
|
34
|
+
self.current_status = 0
|
35
|
+
self.execution_time = 0
|
36
|
+
self.last_succesful_completion = nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/exodus/version.rb
CHANGED
@@ -0,0 +1,187 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require File.dirname(__FILE__) + "/../../lib/exodus"
|
3
|
+
|
4
|
+
describe Exodus::Migration do
|
5
|
+
|
6
|
+
describe "New Oject" do
|
7
|
+
subject { Exodus::Migration.new }
|
8
|
+
|
9
|
+
it "should have a status" do
|
10
|
+
subject.status.should_not be_nil
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should have default value for [status_complete, rerunnable_safe]" do
|
14
|
+
subject.status_complete.should == 1
|
15
|
+
subject.rerunnable_safe.should be_false
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "class methods" do
|
20
|
+
subject { Exodus::Migration.new }
|
21
|
+
|
22
|
+
describe "#inherited" do
|
23
|
+
it "should add a new migrations when a new migration class is created" do
|
24
|
+
migration_size = subject.class.load_all([]).size.to_i
|
25
|
+
class Migration_test1 < Exodus::Migration; end
|
26
|
+
|
27
|
+
subject.class.load_all([]).size.should == migration_size + 1
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "#load_all" do
|
32
|
+
it "should override migrations" do
|
33
|
+
first_migration = subject.class.load_all([]).first.first
|
34
|
+
subject.class.load_all([[first_migration, {:test_args => ['some', 'test', 'arguments']}]]).should include [first_migration, {:test_args => ['some', 'test', 'arguments']}]
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should add a new migrations if the migration is not present" do
|
38
|
+
migration_classes = subject.class.load_all([]).map{|migration, args| migration}
|
39
|
+
class CompleteNewMigration < Exodus::Migration; end
|
40
|
+
|
41
|
+
reloaded_migration_classes = subject.class.load_all([[CompleteNewMigration.name]]).map{|migration, args| migration}
|
42
|
+
reloaded_migration_classes.should include CompleteNewMigration
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "instance methods" do
|
48
|
+
before do
|
49
|
+
class Migration_test1 < Exodus::Migration
|
50
|
+
def up
|
51
|
+
step("Creating new APIUser entity", 1) {UserSupport.create(:name =>'testor')}
|
52
|
+
end
|
53
|
+
|
54
|
+
def down
|
55
|
+
step("Droping APIUser entity", 0) do
|
56
|
+
user = UserSupport.first
|
57
|
+
user.destroy if user
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
subject { Migration_test1.first_or_create({}) }
|
64
|
+
|
65
|
+
describe "#run" do
|
66
|
+
it "should create a new APIUser when running it up" do
|
67
|
+
Migration_test1.collection.drop
|
68
|
+
UserSupport.collection.drop
|
69
|
+
|
70
|
+
lambda{ subject.run('up')}.should change { UserSupport.count }.by(1)
|
71
|
+
subject.status.arguments.should be_empty
|
72
|
+
subject.status.current_status.should == 1
|
73
|
+
subject.status.direction.should == 'up'
|
74
|
+
subject.status.execution_time.should > 0
|
75
|
+
subject.status.last_succesful_completion.should
|
76
|
+
subject.status.message.should == 'Creating new APIUser entity'
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should delete an APIUser when running it down" do
|
80
|
+
Migration_test1.collection.drop
|
81
|
+
UserSupport.collection.drop
|
82
|
+
subject.run('up')
|
83
|
+
|
84
|
+
lambda{ subject.run('down')}.should change { UserSupport.count }.by(-1)
|
85
|
+
subject.status.arguments.should be_empty
|
86
|
+
subject.status.current_status.should == 0
|
87
|
+
subject.status.direction.should == 'down'
|
88
|
+
subject.status.execution_time.should > 0
|
89
|
+
subject.status.message.should == 'Droping APIUser entity'
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "#failure=" do
|
94
|
+
it "should save error information" do
|
95
|
+
exception = nil
|
96
|
+
|
97
|
+
begin
|
98
|
+
raise StandardError "This is an error"
|
99
|
+
rescue Exception => e
|
100
|
+
subject.failure = e
|
101
|
+
exception = e
|
102
|
+
end
|
103
|
+
|
104
|
+
subject.status.error.error_message.should == exception.message
|
105
|
+
subject.status.error.error_class.should == exception.class.name
|
106
|
+
subject.status.error.error_backtrace.should == exception.backtrace
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe "#time_it" do
|
111
|
+
it "should execute a block and set the execution_time" do
|
112
|
+
Migration_test1.collection.drop
|
113
|
+
UserSupport.collection.drop
|
114
|
+
|
115
|
+
lambda do
|
116
|
+
time = subject.send(:time_it) {UserSupport.create(:name => 'testor')}
|
117
|
+
end.should change { UserSupport.count }.by(1)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe "#completed?" do
|
122
|
+
it "should be false when the job is not completed" do
|
123
|
+
Migration_test1.collection.drop
|
124
|
+
UserSupport.collection.drop
|
125
|
+
|
126
|
+
subject.completed?('up').should be_false
|
127
|
+
subject.completed?('down').should be_false
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should be completed up when the job has ran up" do
|
131
|
+
subject.run('up')
|
132
|
+
|
133
|
+
subject.completed?('up').should be_true
|
134
|
+
subject.completed?('down').should be_false
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should be completed down when the job has ran down" do
|
138
|
+
subject.run('down')
|
139
|
+
|
140
|
+
subject.completed?('up').should be_false
|
141
|
+
subject.completed?('down').should be_true
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe "#is_runnable?" do
|
146
|
+
it "should only be runable up when it has never run before" do
|
147
|
+
Migration_test1.collection.drop
|
148
|
+
UserSupport.collection.drop
|
149
|
+
|
150
|
+
subject.is_runnable?('up').should be_true
|
151
|
+
subject.is_runnable?('down').should be_false
|
152
|
+
end
|
153
|
+
|
154
|
+
it "should not be runable up when it has ran up before" do
|
155
|
+
subject.run('up')
|
156
|
+
|
157
|
+
subject.is_runnable?('up').should be_false
|
158
|
+
subject.is_runnable?('down').should be_true
|
159
|
+
end
|
160
|
+
|
161
|
+
it "should not be runable down when it has ran down before" do
|
162
|
+
subject.run('down')
|
163
|
+
|
164
|
+
subject.is_runnable?('up').should be_true
|
165
|
+
subject.is_runnable?('down').should be_false
|
166
|
+
end
|
167
|
+
|
168
|
+
it "should be runable when if the task is safe" do
|
169
|
+
subject.rerunnable_safe = true
|
170
|
+
|
171
|
+
subject.is_runnable?('up').should be_true
|
172
|
+
subject.is_runnable?('down').should be_true
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
describe "MigrationFramework" do
|
178
|
+
describe "sort_migrations" do
|
179
|
+
it "should return the migrations sorted by migration number" do
|
180
|
+
CompleteNewMigration.migration_number = 10
|
181
|
+
sorted_migrations = Exodus::sort_migrations(Exodus::Migration.load_all([]))
|
182
|
+
migrations_number = sorted_migrations.map {|migration, args| migration.migration_number }
|
183
|
+
migrations_number.should == migrations_number.sort
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../lib/exodus'
|
2
|
+
Dir["#{File.dirname(__FILE__)}/support/*.rb"].each { |f| require f }
|
3
|
+
mongo_uri = 'mongodb://exodus:exodus@dharma.mongohq.com:10048/Exodus-test'
|
4
|
+
|
5
|
+
Exodus.configure do |config|
|
6
|
+
config.db = 'Exodus-test'
|
7
|
+
config.connection = Mongo::MongoClient.from_uri(mongo_uri)
|
8
|
+
config.config_file = File.dirname(__FILE__) + '/support/config.yml'
|
9
|
+
end
|
10
|
+
|
data/tasks/exodus.rake
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
def time_it(task, &block)
|
2
|
+
puts "#{task} starting..."
|
3
|
+
start = Time.now
|
4
|
+
yield
|
5
|
+
puts "#{task} Done in (#{Time.now-start}s)!!"
|
6
|
+
end
|
7
|
+
|
8
|
+
def step
|
9
|
+
ENV['STEP']
|
10
|
+
end
|
11
|
+
|
12
|
+
task :require_env do
|
13
|
+
require 'csv'
|
14
|
+
require File.dirname(__FILE__) + '/../lib/exodus'
|
15
|
+
end
|
16
|
+
|
17
|
+
namespace :db do
|
18
|
+
desc "Migrate the database"
|
19
|
+
task :migrate => :require_env do
|
20
|
+
time_it "db:migrate#{" step #{step}" if step}" do
|
21
|
+
migrations = Migration.load_all(Exodus.migrations_info.migrate)
|
22
|
+
Exodus::run_migrations('up', migrations, step)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
desc "Rolls the database back to the previous version"
|
27
|
+
task :rollback => :require_env do
|
28
|
+
time_it "db:rollback#{" step #{step}" if step}" do
|
29
|
+
migrations = Migration.load_all(Exodus.migrations_info.rollback)
|
30
|
+
Exodus::run_migrations('down', migrations, step)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
namespace :migrate do
|
35
|
+
desc "Manually migrates specified migrations (specify migrations or use config/migration.yml)"
|
36
|
+
task :custom, [:migrations_info] => :require_env do |t, args|
|
37
|
+
time_it "db:migrate_custom#{" step #{step}" if step}" do
|
38
|
+
migrations = if args[:migrations_info]
|
39
|
+
YAML.load(args[:migrations_info])
|
40
|
+
else
|
41
|
+
Migration.load_custom(Exodus.migrations_info.migrate_custom)
|
42
|
+
end
|
43
|
+
|
44
|
+
Exodus::run_migrations('up', migrations, step)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
desc "Lists all the migrations"
|
49
|
+
task :list => :require_env do
|
50
|
+
Migration.list
|
51
|
+
end
|
52
|
+
|
53
|
+
desc "Loads migration.yml and displays it"
|
54
|
+
task :yml_status => :require_env do
|
55
|
+
pp Exodus.migrations_info.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
desc "Displays the current status of migrations"
|
59
|
+
task :status => :require_env do
|
60
|
+
Migration.db_status
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
namespace :rollback do
|
65
|
+
desc "Manually rolls the database back using specified migrations (specify migrations or use config/migration.yml)"
|
66
|
+
task :custom, [:migrations_info] => :require_env do |t, args|
|
67
|
+
time_it "db:rollback_custom#{" step #{step}" if step}" do
|
68
|
+
migrations = if args[:migrations_info]
|
69
|
+
YAML.load(args[:migrations_info])
|
70
|
+
else
|
71
|
+
Migration.load_custom(Exodus.migrations_info.rollback_custom)
|
72
|
+
end
|
73
|
+
|
74
|
+
Exodus::run_migrations('down', migrations, step)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: exodus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-05-
|
12
|
+
date: 2013-05-28 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: mongo_mapper
|
@@ -75,7 +75,7 @@ dependencies:
|
|
75
75
|
- - ! '>='
|
76
76
|
- !ruby/object:Gem::Version
|
77
77
|
version: '0'
|
78
|
-
description: Exodus is a migration framework for
|
78
|
+
description: Exodus is a migration framework for MongoDb
|
79
79
|
email:
|
80
80
|
- thomas@fanhattan.com
|
81
81
|
- thomas.dmytryk@supinfo.com
|
@@ -84,13 +84,26 @@ extensions: []
|
|
84
84
|
extra_rdoc_files: []
|
85
85
|
files:
|
86
86
|
- .gitignore
|
87
|
+
- .rspec
|
88
|
+
- .rvmrc
|
89
|
+
- .travis.yml
|
90
|
+
- CHANGELOG.md
|
87
91
|
- Gemfile
|
88
92
|
- LICENSE
|
89
93
|
- README.md
|
90
94
|
- Rakefile
|
91
95
|
- exodus.gemspec
|
92
96
|
- lib/exodus.rb
|
97
|
+
- lib/exodus/config/migration_info.rb
|
98
|
+
- lib/exodus/migrations/migration.rb
|
99
|
+
- lib/exodus/migrations/migration_error.rb
|
100
|
+
- lib/exodus/migrations/migration_status.rb
|
93
101
|
- lib/exodus/version.rb
|
102
|
+
- spec/exodus/migration_spec.rb
|
103
|
+
- spec/spec_helper.rb
|
104
|
+
- spec/support/config.yml
|
105
|
+
- spec/support/user_support.rb
|
106
|
+
- tasks/exodus.rake
|
94
107
|
homepage: ''
|
95
108
|
licenses:
|
96
109
|
- MIT
|
@@ -116,4 +129,8 @@ rubygems_version: 1.8.23
|
|
116
129
|
signing_key:
|
117
130
|
specification_version: 3
|
118
131
|
summary: Exodus uses mongomapper to provide a complete migration framework
|
119
|
-
test_files:
|
132
|
+
test_files:
|
133
|
+
- spec/exodus/migration_spec.rb
|
134
|
+
- spec/spec_helper.rb
|
135
|
+
- spec/support/config.yml
|
136
|
+
- spec/support/user_support.rb
|