migratrix 0.0.5
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/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: []
|