exodus 1.0.0 → 1.0.1
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/.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
|