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 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: []