migratrix 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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,2 @@
1
+ #!/usr/bin/env ruby
2
+ puts "Nothing to see here yet -- use rake tasks to call Migratrix.migrate!(:migrator, options) directly"
@@ -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,8 @@
1
+ module Migratrix
2
+ # ----------------------------------------------------------------------
3
+ # Exceptions
4
+ class MigrationAlreadyExists < Exception; end
5
+ class MigrationFileNotFound < Exception; end
6
+ class MigrationNotDefined < Exception; end
7
+ end
8
+
@@ -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,10 @@
1
+ class Object
2
+ def in?(array)
3
+ array.include? self
4
+ end
5
+
6
+ def deep_copy
7
+ Marshal::load(Marshal::dump(self))
8
+ end
9
+ end
10
+
@@ -0,0 +1,11 @@
1
+ # String extensions
2
+ class String
3
+ def plural?
4
+ self != self.singularize
5
+ end
6
+
7
+ def singular?
8
+ self != self.pluralize
9
+ end
10
+ end
11
+
@@ -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,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Migratrix::Migratrix do
4
+ it "should exist" do
5
+ Migratrix::Migratrix.should_not be_nil
6
+ end
7
+ end
@@ -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
@@ -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: []