migratrix 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +22 -0
- data/README.md +144 -0
- data/bin/migratrix +2 -0
- data/lib/migratrix/active_record_migration_helpers.rb +57 -0
- data/lib/migratrix/exceptions.rb +8 -0
- data/lib/migratrix/migration.rb +52 -0
- data/lib/migratrix/migratrix.rb +98 -0
- data/lib/migratrix.rb +18 -0
- data/lib/patches/andand.rb +104 -0
- data/lib/patches/object_ext.rb +10 -0
- data/lib/patches/string_ext.rb +11 -0
- data/spec/fixtures/migrations/marbles_migration.rb +17 -0
- data/spec/lib/migration_spec.rb +47 -0
- data/spec/lib/migrator_spec.rb +7 -0
- data/spec/lib/migratrix_spec.rb +117 -0
- data/spec/patches/object_ext_spec.rb +31 -0
- data/spec/patches/string_ext_spec.rb +26 -0
- data/spec/spec_helper.rb +39 -0
- metadata +84 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2008-2009 David Brady github@shinybit.com
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
# Migratrix
|
2
|
+
|
3
|
+
Dominate your legacy Rails migrations! Migratrix is a gem to help you
|
4
|
+
generate and control Migrations, which extract data from legacy systems
|
5
|
+
and import them into your current one.
|
6
|
+
|
7
|
+
## Warning: Experimental Developmental In-Progress Stuff
|
8
|
+
|
9
|
+
I am currently extracting Migratrix from an ancient legacy codebase.
|
10
|
+
(Oh the irony.) A lot of the stuff I say Migratrix supports is stuff
|
11
|
+
that it supports over there, not in here. Be aware that most of this
|
12
|
+
document is more of a TODO list than a statement of fact. I'll remove
|
13
|
+
this message once Migratrix does what it says on the tin.
|
14
|
+
|
15
|
+
## General Info
|
16
|
+
|
17
|
+
Migratrix is a legacy migration tool strategy
|
18
|
+
|
19
|
+
## Motivation
|
20
|
+
|
21
|
+
So... much... legacy... data....
|
22
|
+
|
23
|
+
## Rails and Ruby Requirements
|
24
|
+
|
25
|
+
### Rails 3 Only
|
26
|
+
|
27
|
+
Migratrix was originally developed under Rails 2, but come on. Rails 2
|
28
|
+
apps are legacy SOURCES now, not destinations. Migratrix requires
|
29
|
+
Rails 3. Once everything's in place I'll bump the Migratrix version to
|
30
|
+
3.x to indicate that Migratrix is in keeping with Rails 3.
|
31
|
+
|
32
|
+
### Ruby 1.9
|
33
|
+
|
34
|
+
Because I can.
|
35
|
+
|
36
|
+
## Example
|
37
|
+
|
38
|
+
## ETL
|
39
|
+
|
40
|
+
I use the term "ETL" here in a loosely similar mechanism as in data
|
41
|
+
warehousing: Extract, Transform and Load. Migratrix approaches
|
42
|
+
migrations in three phases:
|
43
|
+
|
44
|
+
* **Extract** The Migration obtains the legacy data from 1 or more
|
45
|
+
sources
|
46
|
+
|
47
|
+
* **Transform** The Migration transforms the data into 1 or more
|
48
|
+
outputs
|
49
|
+
|
50
|
+
* **Load** The Migration saves the data into the new database or other
|
51
|
+
output(s)
|
52
|
+
|
53
|
+
|
54
|
+
## Migration Dependencies
|
55
|
+
|
56
|
+
Migratrix isn't quite smart enough to know that a migrator depends on
|
57
|
+
another migrator. (Actually, that's easy. What's hard is knowing if a
|
58
|
+
dependent migrator has run and is up-to-date.)
|
59
|
+
|
60
|
+
## Strategies
|
61
|
+
|
62
|
+
Migratrix supports multiple migration strategies:
|
63
|
+
|
64
|
+
* Straight up ActiveRecord: Given a model class (probably connected to
|
65
|
+
a legacy database), a mapping transform and a destination model,
|
66
|
+
Migratrix extracts the source models, transforms them and saves
|
67
|
+
them. (In Rails 2 this was sometimes dangerous because the entire
|
68
|
+
source table(s) would be loaded into memory during the Extract
|
69
|
+
phase. In Rails 3 this is no longer a problem, somewhat reducing the
|
70
|
+
motivation for the next strategy.)
|
71
|
+
|
72
|
+
* Batch Select: Given a SQL query as the extraction source, a
|
73
|
+
Migration can pull batches of hashes from the legacy database. You
|
74
|
+
give up having legacy model objects in exchange for being able to
|
75
|
+
scan massive tables in batches of 1000 or more (or less). In Rails 3
|
76
|
+
this is less of a motivation. However, for complicated source
|
77
|
+
extractions where a source model does not make sense, this strategy
|
78
|
+
is still excellent.
|
79
|
+
|
80
|
+
* Pure SQL: If both databases are on the same server, you can often
|
81
|
+
migrate data with a +SELECT INTO+ or +INSERT SELECT+ statement. This
|
82
|
+
is one of the fastest ways to migrate data because the data never
|
83
|
+
comes up into the ruby data space. On the other hand, the migration
|
84
|
+
may be more complicated to write because the data can be manipulated
|
85
|
+
by ruby--the entire ETL must be handled by the single SQL statement.
|
86
|
+
Additionally, it is seriously nontrivial to log the migrated records
|
87
|
+
because you have to handle that at the SQL level as well.
|
88
|
+
|
89
|
+
## Slices of Data
|
90
|
+
|
91
|
+
Migratrix supports taking partial slices of data. You can migrate a
|
92
|
+
single record to test a migraton, grab 100 records or 1000 to get an
|
93
|
+
idea for how long a full migration will take, or perform the entire
|
94
|
+
migration.
|
95
|
+
|
96
|
+
## Ongoing Migrations
|
97
|
+
|
98
|
+
Migratrix also supports ongoing migrations, which are useful when the
|
99
|
+
legacy database continues to operate and change after the new Rails
|
100
|
+
site is live and you cannot remigrate all-new data.
|
101
|
+
|
102
|
+
## Migration Log
|
103
|
+
|
104
|
+
Migratrix can create a migration log for you. This is a table that
|
105
|
+
contains the legacy id and table, and the destination id and table. It
|
106
|
+
can also record the source object, which is useful for debugging or
|
107
|
+
handling migration cases where legacy records get changed or deleted
|
108
|
+
after migrating.
|
109
|
+
|
110
|
+
## Migration Tests
|
111
|
+
|
112
|
+
Sorry, nothing to see here yet. Migratrix was originally developed in
|
113
|
+
an environment where the migrations were so heavy-duty and hairy that
|
114
|
+
we literally had `legacy_migration_test` and
|
115
|
+
`legacy_migration_development` databases for migration development.
|
116
|
+
Migratrix doesn't directly support testing of migrations yet. This is
|
117
|
+
mostly a note to remind myself that that heavy-duty migrations can and
|
118
|
+
should be developed in a TDD style, and Migratrix should make this
|
119
|
+
easy.
|
120
|
+
|
121
|
+
## A note about the name
|
122
|
+
|
123
|
+
In old Latin, -or versus -ix endings aren't just about feminine and
|
124
|
+
masculine. Like old Greek's -a versus -os endings, a masculine ending
|
125
|
+
usually refers to a small, single instance of a thing while the
|
126
|
+
feminine refers to the large or collective instance. For example, in
|
127
|
+
Greek, the masculine word _petros_ means "stone" or "pebble" while the
|
128
|
+
feminine word _petra_ means "bedrock". More poetically, the feminine
|
129
|
+
can sometimes be viewed not as a gender mirror, but maternally
|
130
|
+
instead: the bedrock is what creates or gives birth to stones and
|
131
|
+
pebbles.
|
132
|
+
|
133
|
+
Hence Migratrix is the gem that helps you generate and control your
|
134
|
+
Migrations, while Migration is the class that defines how a single
|
135
|
+
set of data, such as a table, a set of models, etc., is migrated.
|
136
|
+
|
137
|
+
## License
|
138
|
+
|
139
|
+
MIT. See the license file.
|
140
|
+
|
141
|
+
## Authors
|
142
|
+
|
143
|
+
* David Brady -- github@shinybit.com
|
144
|
+
|
data/bin/migratrix
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
module Migratrix
|
2
|
+
module ActiveRecordMigrationHelpers
|
3
|
+
|
4
|
+
# Executes a query on this class' connection, and logs the query
|
5
|
+
# or an optional message to Migratrix.log.
|
6
|
+
def execute(query, msg=nil)
|
7
|
+
Migratrix.log(msg || query) unless msg == false
|
8
|
+
connection.execute query
|
9
|
+
end
|
10
|
+
|
11
|
+
# MySQL ONLY: truncates a table using TRUNCATE, which drops the
|
12
|
+
# data very quickly and resets any autoindexing primary key to 1.
|
13
|
+
def mysql_truncate(table)
|
14
|
+
execute("TRUNCATE #{table}")
|
15
|
+
end
|
16
|
+
|
17
|
+
# PostGreSQL ONLY: truncates a table by deleting all its rows and
|
18
|
+
# restarting its id sequence at 1.
|
19
|
+
#
|
20
|
+
# Note: TRUNCATE was added to PostGreSQL in version 8.3, which at
|
21
|
+
# the time of this writing is still poorly adopted. This code
|
22
|
+
# works on earlier versions, is MVCC-safe, and will trigger
|
23
|
+
# cascading deletes.
|
24
|
+
#
|
25
|
+
# It does NOT, however, actually look up the table's sequence
|
26
|
+
# definition. It assumes the sequence for a is named a_id_seq and
|
27
|
+
# that it should be reset to 1. (A tiny dash of extra cleverness
|
28
|
+
# is all that would be needed to read start_value from the
|
29
|
+
# sequence, but for now this is a pure-SQL, stateless call.)
|
30
|
+
def psql_truncate(table)
|
31
|
+
execute("DELETE FROM #{table}; ALTER SEQUENCE #{table}_id_seq RESTART WITH 1")
|
32
|
+
end
|
33
|
+
|
34
|
+
# MySQL ONLY: Disables indexes on a table and locks it for
|
35
|
+
# writing, optionally read-locks another list of tables, then
|
36
|
+
# yields to the given block before unlocking. This prevents MySQL
|
37
|
+
# from indexing the migrated data until the block is complete.
|
38
|
+
# This produces a significant speedup on InnoDB tables with
|
39
|
+
# multiple indexes. (The plural of anecdote is not data, but on
|
40
|
+
# one heavily-indexed table, migrating 10 million records took 38
|
41
|
+
# hours with indexes enabled and under 2 hours with them
|
42
|
+
# disabled.)
|
43
|
+
def with_mysql_indexes_disabled_on(table, *read_locked_tables, &block)
|
44
|
+
log "Locking table '#{table}' and disabling indexes..."
|
45
|
+
lock_cmd = "LOCK TABLES `#{table}` WRITE"
|
46
|
+
if read_locked_tables.andand.size > 0
|
47
|
+
lock_cmd += ', ' + (read_locked_tables.map {|t| "#{t} READ"} * ", ")
|
48
|
+
end
|
49
|
+
execute lock_cmd
|
50
|
+
execute("/*!40000 ALTER TABLE `#{table}` DISABLE KEYS */")
|
51
|
+
yield
|
52
|
+
log "Unlocking table '#{table}' and re-enabling indexes..."
|
53
|
+
execute("/*!40000 ALTER TABLE `#{table}` ENABLE KEYS */")
|
54
|
+
execute("UNLOCK TABLES")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Migratrix
|
2
|
+
# Superclass for all migrations. Migratrix COULD check to see that a
|
3
|
+
# loaded migration inherits from this class, but hey, duck typing.
|
4
|
+
class Migration
|
5
|
+
attr_accessor :options, :logger
|
6
|
+
|
7
|
+
def initialize(options={})
|
8
|
+
# cannot make a deep copy of an IO stream (e.g. logger) so make a shallow copy of it and move it out of the way
|
9
|
+
@options = options.dup.tap {|h| @logger = h.delete("logger")}.deep_copy
|
10
|
+
|
11
|
+
# if options["logger"]
|
12
|
+
# @logger = options["logger"]
|
13
|
+
# options = options.dup
|
14
|
+
# options.delete["logger"]
|
15
|
+
# end
|
16
|
+
# @options = options.deep_copy
|
17
|
+
# This should only be loaded if a) the Migration uses the AR
|
18
|
+
# extract strategy and b) it's not already loaded
|
19
|
+
# ::ActiveRecord::Base.send(:include, MigrationHelpers) unless ::ActiveRecord::Base.const_defined?("MigrationHelpers")
|
20
|
+
end
|
21
|
+
|
22
|
+
# Load this data from source
|
23
|
+
def extract
|
24
|
+
# run the chain of extractions
|
25
|
+
end
|
26
|
+
|
27
|
+
# Transforms source data into outputs
|
28
|
+
def transform
|
29
|
+
# run the chain of transforms
|
30
|
+
end
|
31
|
+
|
32
|
+
# Saves the migrated data by "loading" it into our database or
|
33
|
+
# other data sink.
|
34
|
+
def load
|
35
|
+
# run the chain of loads
|
36
|
+
end
|
37
|
+
|
38
|
+
# Perform the migration
|
39
|
+
def migrate
|
40
|
+
extract
|
41
|
+
transform
|
42
|
+
load
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.log(msg="", level=:info)
|
46
|
+
return unless logger
|
47
|
+
level = :info unless level.in? [:debug, :info, :warn, :error, :fatal, :unknown]
|
48
|
+
logger.send level, "#{Time.now.strftime('%T')}: #{msg}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# Main "App" or Driver class for Migrating. Responsible for loading
|
2
|
+
# and integrating all the parts of a migration.
|
3
|
+
|
4
|
+
module Migratrix
|
5
|
+
|
6
|
+
def migrate(name, options={})
|
7
|
+
::Migratrix::Migratrix.migrate(name, options)
|
8
|
+
end
|
9
|
+
|
10
|
+
class Migratrix
|
11
|
+
def self.migrate(name, options={})
|
12
|
+
migratrix = self.new()
|
13
|
+
migration = migratrix.create_migration(name, options)
|
14
|
+
migration.migrate
|
15
|
+
migratrix
|
16
|
+
end
|
17
|
+
|
18
|
+
# Loads #{name}_migration.rb from migrations path, instantiates
|
19
|
+
# #{Name}Migration with options, and returns it.
|
20
|
+
def create_migration(name, options={})
|
21
|
+
options = filter_options(options)
|
22
|
+
klass_name = migration_name(name)
|
23
|
+
unless loaded?(klass_name)
|
24
|
+
raise MigrationAlreadyExists.new("Migratrix cannot instantiate class Migratrix::#{klass_name} because it already exists") if ::Migratrix.const_defined?(klass_name)
|
25
|
+
filename = migrations_path + "#{name}_migration.rb"
|
26
|
+
raise MigrationFileNotFound.new("Migratrix cannot find migration file #{filename}") unless File.exists?(filename)
|
27
|
+
load filename
|
28
|
+
raise MigrationNotDefined.new("Expected migration file #{filename} to define Migratrix::#{klass_name} but it did not") unless ::Migratrix.const_defined?(klass_name)
|
29
|
+
register_migration(klass_name, "Migratrix::#{klass_name}".constantize)
|
30
|
+
end
|
31
|
+
fetch_migration(klass_name).new(options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def migration_name(name)
|
35
|
+
name = name.to_s
|
36
|
+
name = if name.plural?
|
37
|
+
name.classify.pluralize
|
38
|
+
else
|
39
|
+
name.classify
|
40
|
+
end
|
41
|
+
name + "Migration"
|
42
|
+
end
|
43
|
+
|
44
|
+
def filter_options(hash)
|
45
|
+
Hash[valid_options.map {|v| hash.key?(v) ? [v, hash[v]] : nil }.compact]
|
46
|
+
end
|
47
|
+
|
48
|
+
def valid_options
|
49
|
+
%w(limit where logger)
|
50
|
+
end
|
51
|
+
|
52
|
+
# ----------------------------------------------------------------------
|
53
|
+
# Candidate for exract class? MigrationRegistry?
|
54
|
+
def loaded?(name)
|
55
|
+
registered_migrations.key? name.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
def register_migration(name, klass)
|
59
|
+
registered_migrations[name.to_s] = klass
|
60
|
+
end
|
61
|
+
|
62
|
+
def fetch_migration(name)
|
63
|
+
registered_migrations.fetch name.to_s
|
64
|
+
end
|
65
|
+
|
66
|
+
def registered_migrations
|
67
|
+
@registered_migrations ||= {}
|
68
|
+
end
|
69
|
+
# End MigrationRegistry
|
70
|
+
# ----------------------------------------------------------------------
|
71
|
+
|
72
|
+
# ----------------------------------------------------------------------
|
73
|
+
# Migration path class accessors. Defaults to lib/migrations.
|
74
|
+
def migrations_path
|
75
|
+
@migrations_path ||= ::Migratrix::DEFAULT_MIGRATIONS_PATH
|
76
|
+
end
|
77
|
+
|
78
|
+
def migrations_path=(new_path)
|
79
|
+
@migrations_path = Pathname.new new_path
|
80
|
+
end
|
81
|
+
|
82
|
+
# def self.migrations_path
|
83
|
+
# @@migrations_path ||= ::Migratrix::DEFAULT_MIGRATIONS_PATH
|
84
|
+
# end
|
85
|
+
|
86
|
+
# def self.migrations_path=(new_path)
|
87
|
+
# @@migrations_path = Pathname.new new_path
|
88
|
+
# end
|
89
|
+
# End Migration path management
|
90
|
+
# ----------------------------------------------------------------------
|
91
|
+
|
92
|
+
# def self.log(msg="", level=:info)
|
93
|
+
# return if quiet
|
94
|
+
# level = :info unless level.in? [:debug, :info, :warn, :error, :fatal, :unknown]
|
95
|
+
# logger.send level, "#{Time.now.strftime('%T')}: #{msg}"
|
96
|
+
# end
|
97
|
+
end
|
98
|
+
end
|
data/lib/migratrix.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Migratrix
|
5
|
+
APP=Pathname.new(__FILE__).dirname + "migratrix"
|
6
|
+
EXT=Pathname.new(__FILE__).dirname + "patches"
|
7
|
+
|
8
|
+
DEFAULT_MIGRATIONS_PATH = Rails.root + 'lib/migrations'
|
9
|
+
|
10
|
+
|
11
|
+
require EXT + 'string_ext'
|
12
|
+
require EXT + 'object_ext'
|
13
|
+
require EXT + 'andand'
|
14
|
+
require APP + 'exceptions'
|
15
|
+
require APP + 'migration'
|
16
|
+
require APP + 'migratrix'
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
This code was hand-patched by David Brady in May 2011 to shut up the
|
4
|
+
warning in Ruby 1.9. At the time of this writing there is a
|
5
|
+
6-month-old patch for this in Reg's github version of andand but the
|
6
|
+
gem still does not support it. This is minor tweak; Reg's copyright
|
7
|
+
and license remain unchanged.
|
8
|
+
|
9
|
+
Copyright (c) 2008 Reginald Braithwaite
|
10
|
+
http://weblog.raganwald.com/2008/01/objectandand-objectme-in-ruby.html
|
11
|
+
|
12
|
+
Permission is hereby granted, free of charge, to any person
|
13
|
+
obtaining a copy of this software and associated documentation
|
14
|
+
files (the "Software"), to deal in the Software without
|
15
|
+
restriction, including without limitation the rights to use,
|
16
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
17
|
+
copies of the Software, and to permit persons to whom the
|
18
|
+
Software is furnished to do so, subject to the following
|
19
|
+
conditions:
|
20
|
+
|
21
|
+
The above copyright notice and this permission notice shall be
|
22
|
+
included in all copies or substantial portions of the Software.
|
23
|
+
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
25
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
26
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
27
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
28
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
29
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
30
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
31
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
32
|
+
|
33
|
+
Some code adapted from Jim Weirich's post:
|
34
|
+
http://onestepback.org/index.cgi/Tech/Ruby/BlankSlate.rdoc
|
35
|
+
|
36
|
+
=end
|
37
|
+
module AndAnd
|
38
|
+
# :nocov:
|
39
|
+
|
40
|
+
module ObjectGoodies
|
41
|
+
|
42
|
+
def andand (p = nil)
|
43
|
+
if self
|
44
|
+
if block_given?
|
45
|
+
yield(self)
|
46
|
+
elsif p
|
47
|
+
p.to_proc.call(self)
|
48
|
+
else
|
49
|
+
self
|
50
|
+
end
|
51
|
+
else
|
52
|
+
if block_given? or p
|
53
|
+
self
|
54
|
+
else
|
55
|
+
MockReturningMe.new(self)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def me (p = nil)
|
61
|
+
if block_given?
|
62
|
+
yield(self)
|
63
|
+
self
|
64
|
+
elsif p
|
65
|
+
p.to_proc.call(self)
|
66
|
+
self
|
67
|
+
else
|
68
|
+
ProxyReturningMe.new(self)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
class Object
|
77
|
+
include AndAnd::ObjectGoodies
|
78
|
+
end
|
79
|
+
|
80
|
+
module AndAnd
|
81
|
+
|
82
|
+
class BlankSlate
|
83
|
+
instance_methods.reject { |m| m =~ /^(__|object_id$)/ }.each { |m| undef_method m }
|
84
|
+
def initialize(me)
|
85
|
+
@me = me
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class MockReturningMe < BlankSlate
|
90
|
+
def method_missing(*args)
|
91
|
+
@me
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class ProxyReturningMe < BlankSlate
|
96
|
+
def method_missing(sym, *args, &block)
|
97
|
+
@me.__send__(sym, *args, &block)
|
98
|
+
@me
|
99
|
+
end
|
100
|
+
end
|
101
|
+
#:nocov:
|
102
|
+
|
103
|
+
end
|
104
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Migratrix
|
2
|
+
# Fake migration fixture for "Marbles"
|
3
|
+
class MarblesMigration < Migration
|
4
|
+
def initialize(options={})
|
5
|
+
super
|
6
|
+
@@migrated = false
|
7
|
+
end
|
8
|
+
|
9
|
+
def migrate
|
10
|
+
@@migrated = true
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.migrated?
|
14
|
+
@@migrated
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# This migration is embedded in migration_spec.rb to allow testing of
|
4
|
+
# the class methods that specialize subclasses.
|
5
|
+
class Migratrix::TestMigration < Migratrix::Migration
|
6
|
+
|
7
|
+
end
|
8
|
+
|
9
|
+
describe Migratrix::Migration do
|
10
|
+
describe ".new" do
|
11
|
+
it "does not modify given options hash" do
|
12
|
+
conditions = ["id=? AND approved=?", 42, true]
|
13
|
+
migration = Migratrix::TestMigration.new({ "where" => conditions })
|
14
|
+
|
15
|
+
migration.options["where"][0] += " AND pants=?"
|
16
|
+
migration.options["where"] << false
|
17
|
+
migration.options["where"].should == ["id=? AND approved=? AND pants=?", 42, true, false]
|
18
|
+
conditions.should == ["id=? AND approved=?", 42, true]
|
19
|
+
end
|
20
|
+
|
21
|
+
it "safely moves logger option out of its options and into logger attribute" do
|
22
|
+
conditions = ["id=? AND approved=?", 42, true]
|
23
|
+
logger = Logger.new(StringIO.new)
|
24
|
+
options = { "where" => conditions, "logger" => logger }
|
25
|
+
|
26
|
+
migration = Migratrix::TestMigration.new(options)
|
27
|
+
|
28
|
+
migration.options.should_not have_key("logger")
|
29
|
+
migration.logger.should == logger
|
30
|
+
options.should == { "where" => conditions, "logger" => logger }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "#migrate" do
|
35
|
+
let(:migration) { Migratrix::TestMigration.new }
|
36
|
+
|
37
|
+
it "delegates to extract, transform, and load" do
|
38
|
+
migration.should_receive(:extract).once
|
39
|
+
migration.should_receive(:transform).once
|
40
|
+
migration.should_receive(:load).once
|
41
|
+
migration.migrate
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# Migatrix loads Migration classes into its namespace. In order to
|
4
|
+
# test collision prevention, I needed to reach into Migratrix and
|
5
|
+
# mindwipe it of any migrations. Here's the shiv to do that. I
|
6
|
+
# originally put an API to do this on Migratrix but these specs are
|
7
|
+
# the only clients of it so I removed it again. If you find a
|
8
|
+
# legitimate use for it, feel free to re-add a remove_migration
|
9
|
+
# method and send me a patch.
|
10
|
+
def reset_migratrix!(migratrix)
|
11
|
+
Migratrix.constants.map(&:to_s).select {|m| m =~ /.+Migration$/}.each do |migration|
|
12
|
+
Migratrix.send(:remove_const, migration.to_sym)
|
13
|
+
end
|
14
|
+
migratrix.registered_migrations.clear
|
15
|
+
end
|
16
|
+
|
17
|
+
describe Migratrix::Migratrix do
|
18
|
+
let (:migratrix) { Migratrix::Migratrix.new }
|
19
|
+
|
20
|
+
it "exists (sanity check)" do
|
21
|
+
Migratrix.should_not be_nil
|
22
|
+
Migratrix.class.should == Module
|
23
|
+
Migratrix.class.should_not == Class
|
24
|
+
Migratrix::Migratrix.class.should_not == Module
|
25
|
+
Migratrix::Migratrix.class.should == Class
|
26
|
+
Migratrix.const_defined?("Migratrix").should be_true
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "MigrationRegistry (needs to be extracted)" do
|
30
|
+
before do
|
31
|
+
reset_migratrix! migratrix
|
32
|
+
Migratrix.class_eval("class PantsMigration < Migration; end")
|
33
|
+
migratrix.register_migration "PantsMigration", Migratrix::PantsMigration
|
34
|
+
end
|
35
|
+
|
36
|
+
it "can register migrations by name" do
|
37
|
+
migratrix.loaded?("PantsMigration").should be_true
|
38
|
+
Migratrix.const_defined?("PantsMigration").should be_true
|
39
|
+
end
|
40
|
+
|
41
|
+
it "can fetch registered migration class" do
|
42
|
+
migratrix.fetch_migration("PantsMigration").should == Migratrix::PantsMigration
|
43
|
+
end
|
44
|
+
|
45
|
+
it "raises fetch error when fetching unregistered migration" do
|
46
|
+
lambda { migratrix.fetch_migration("arglebargle") }.should raise_error(KeyError)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe ".migrations_path" do
|
51
|
+
it "uses ./lib/migrations by default" do
|
52
|
+
migratrix.migrations_path.should == ROOT + "lib/migrations"
|
53
|
+
end
|
54
|
+
|
55
|
+
it "can be overridden" do
|
56
|
+
migratrix.migrations_path = Pathname.new('/tmp')
|
57
|
+
migratrix.migrations_path.should == Pathname.new("/tmp")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "#valid_options" do
|
62
|
+
it "returns the valid set of option keys" do
|
63
|
+
migratrix.valid_options.should == ["limit", "where", "logger"]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "#filter_options" do
|
68
|
+
it "filters out invalid options" do
|
69
|
+
options = migratrix.filter_options({ "pants" => 42, "limit" => 3})
|
70
|
+
options["limit"].should == 3
|
71
|
+
options.should_not have_key("pants")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "#migration_name" do
|
76
|
+
it "classifies the name and adds Migration" do
|
77
|
+
migratrix.migration_name("shirt").should == "ShirtMigration"
|
78
|
+
end
|
79
|
+
|
80
|
+
it "handles symbols" do
|
81
|
+
migratrix.migration_name(:socks).should == "SocksMigration"
|
82
|
+
end
|
83
|
+
|
84
|
+
it "preserves pluralization" do
|
85
|
+
migratrix.migration_name(:pants).should == "PantsMigration"
|
86
|
+
migratrix.migration_name(:shirts).should == "ShirtsMigration"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "#create_migration" do
|
91
|
+
before do
|
92
|
+
reset_migratrix! migratrix
|
93
|
+
migratrix.migrations_path = SPEC + "fixtures/migrations"
|
94
|
+
end
|
95
|
+
|
96
|
+
it "creates new migration by name with filtered options" do
|
97
|
+
migration = migratrix.create_migration :marbles, { "cheese" => 42, "where" => "id > 100", "limit" => "100" }
|
98
|
+
migration.class.should == Migratrix::MarblesMigration
|
99
|
+
Migratrix::MarblesMigration.should_not be_migrated
|
100
|
+
migration.options.should == { "where" => "id > 100", "limit" => "100" }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe ".migrate" do
|
105
|
+
before do
|
106
|
+
reset_migratrix! migratrix
|
107
|
+
migratrix.migrations_path = SPEC + "fixtures/migrations"
|
108
|
+
end
|
109
|
+
|
110
|
+
it "loads migration and migrates it" do
|
111
|
+
Migratrix::Migratrix.stub!(:new).and_return(migratrix)
|
112
|
+
Migratrix::Migratrix.migrate :marbles
|
113
|
+
Migratrix::MarblesMigration.should be_migrated
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe String do
|
4
|
+
describe "#plural?" do
|
5
|
+
it "identifies plural strings" do
|
6
|
+
"shirts".should be_plural
|
7
|
+
end
|
8
|
+
|
9
|
+
it "identifies collectively plural strings" do
|
10
|
+
"people".should be_plural
|
11
|
+
end
|
12
|
+
|
13
|
+
it "ignores singular strings" do
|
14
|
+
"sock".should_not be_plural
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#singular?" do
|
19
|
+
it "identifies singular strings" do
|
20
|
+
"shirt".should be_singular
|
21
|
+
end
|
22
|
+
|
23
|
+
it "identifies collectively plural strings as singular" do
|
24
|
+
"person".should be_singular
|
25
|
+
end
|
26
|
+
|
27
|
+
it "ignores plural strings" do
|
28
|
+
"socks".should_not be_singular
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Object do
|
4
|
+
describe "#in?" do
|
5
|
+
it "returns true if object is included in collection" do
|
6
|
+
3.should be_in([1,2,3,4])
|
7
|
+
"sexy".should be_in("dylsexyc")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#deep_copy" do
|
12
|
+
let(:ray) { [1,2,3]}
|
13
|
+
let(:hash) { {a: 42, b: 69, c: 13, d: 64} }
|
14
|
+
let(:struct) { Struct.new(:array, :hash).new(ray, hash)}
|
15
|
+
let(:big_hash) { {:struct1 => struct, :struct2 => struct, :hash => hash, :array => ray }}
|
16
|
+
|
17
|
+
describe "sanity check" do
|
18
|
+
it "should have objects correctly aliased" do
|
19
|
+
big_hash[:struct1].object_id.should == big_hash[:struct2].object_id
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# it "returns a completely cloned, fully deep copy of the object" do
|
24
|
+
# end
|
25
|
+
end
|
26
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
if ENV["COVERAGE"]
|
2
|
+
require 'simplecov'
|
3
|
+
SimpleCov.start
|
4
|
+
end
|
5
|
+
|
6
|
+
require 'pathname'
|
7
|
+
require 'ruby-debug'
|
8
|
+
require 'rails'
|
9
|
+
require 'logger'
|
10
|
+
|
11
|
+
# Requires supporting ruby files with custom matchers and macros, etc,
|
12
|
+
# in spec/support/ and its subdirectories.
|
13
|
+
SPEC = Pathname.new(__FILE__).dirname
|
14
|
+
ROOT = SPEC + ".."
|
15
|
+
LIB = ROOT + "lib"
|
16
|
+
|
17
|
+
# Rails is loaded but not actually started. Let's keep it that way as
|
18
|
+
# much as possible. But we DO need Rails.root, so:
|
19
|
+
module Rails
|
20
|
+
def self.root
|
21
|
+
ROOT
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
Dir[SPEC + "support/**/*.rb"].each {|f| require f}
|
26
|
+
|
27
|
+
require LIB + 'migratrix'
|
28
|
+
|
29
|
+
RSpec.configure do |config|
|
30
|
+
# == Mock Framework
|
31
|
+
#
|
32
|
+
# If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
|
33
|
+
#
|
34
|
+
# config.mock_with :mocha
|
35
|
+
# config.mock_with :flexmock
|
36
|
+
# config.mock_with :rr
|
37
|
+
config.mock_with :rspec
|
38
|
+
end
|
39
|
+
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: migratrix
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.5
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- David Brady
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-10-14 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: trollop
|
16
|
+
requirement: &2153520180 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2153520180
|
25
|
+
description: Migratrix, a Rails legacy database migration tool supporting multiple
|
26
|
+
strategies, including arbitrary n-ary migrations (1->n, n->1, n->m), arbitrary inputs
|
27
|
+
and outputs (ActiveRecord, bare SQL, CSV) and migration logging
|
28
|
+
email: github@shinybit.com
|
29
|
+
executables:
|
30
|
+
- migratrix
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files:
|
33
|
+
- README.md
|
34
|
+
- MIT-LICENSE
|
35
|
+
files:
|
36
|
+
- README.md
|
37
|
+
- bin/migratrix
|
38
|
+
- lib/migratrix.rb
|
39
|
+
- lib/migratrix/active_record_migration_helpers.rb
|
40
|
+
- lib/migratrix/exceptions.rb
|
41
|
+
- lib/migratrix/migration.rb
|
42
|
+
- lib/migratrix/migratrix.rb
|
43
|
+
- lib/patches/andand.rb
|
44
|
+
- lib/patches/object_ext.rb
|
45
|
+
- lib/patches/string_ext.rb
|
46
|
+
- spec/fixtures/migrations/marbles_migration.rb
|
47
|
+
- spec/lib/migration_spec.rb
|
48
|
+
- spec/lib/migrator_spec.rb
|
49
|
+
- spec/lib/migratrix_spec.rb
|
50
|
+
- spec/patches/object_ext_spec.rb
|
51
|
+
- spec/patches/string_ext_spec.rb
|
52
|
+
- spec/spec_helper.rb
|
53
|
+
- MIT-LICENSE
|
54
|
+
homepage: http://github.com/dbrady/migratrix/
|
55
|
+
licenses: []
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options:
|
58
|
+
- --line-numbers
|
59
|
+
- --inline-source
|
60
|
+
- --main
|
61
|
+
- README.md
|
62
|
+
- --title
|
63
|
+
- Migratrix - Rails migrations made less icky
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ! '>='
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
requirements: []
|
79
|
+
rubyforge_project:
|
80
|
+
rubygems_version: 1.8.6
|
81
|
+
signing_key:
|
82
|
+
specification_version: 3
|
83
|
+
summary: Rails 3 legacy database migratrion tool supporting multiple strategies
|
84
|
+
test_files: []
|