dm-migrations 0.9.2
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/LICENSE +20 -0
- data/README +4 -0
- data/Rakefile +88 -0
- data/TODO +8 -0
- data/lib/dm-migrations.rb +1 -0
- data/lib/migration.rb +192 -0
- data/lib/migration_runner.rb +88 -0
- data/lib/spec/example/migration_example_group.rb +73 -0
- data/lib/spec/matchers/migration_matchers.rb +107 -0
- data/lib/sql.rb +110 -0
- data/lib/sql/column.rb +9 -0
- data/lib/sql/mysql.rb +52 -0
- data/lib/sql/postgresql.rb +78 -0
- data/lib/sql/sqlite3.rb +50 -0
- data/lib/sql/table.rb +19 -0
- data/spec/integration/migration_runner_spec.rb +78 -0
- data/spec/integration/migration_spec.rb +136 -0
- data/spec/integration/sql_spec.rb +148 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +37 -0
- metadata +82 -0
@@ -0,0 +1,107 @@
|
|
1
|
+
|
2
|
+
module Spec
|
3
|
+
module Matchers
|
4
|
+
module Migration
|
5
|
+
|
6
|
+
def have_table(table_name)
|
7
|
+
HaveTableMatcher.new(table_name)
|
8
|
+
end
|
9
|
+
|
10
|
+
def have_column(column_name)
|
11
|
+
HaveColumnMatcher.new(column_name)
|
12
|
+
end
|
13
|
+
|
14
|
+
def permit_null
|
15
|
+
NullableColumnMatcher.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def be_primary_key
|
19
|
+
PrimaryKeyMatcher.new
|
20
|
+
end
|
21
|
+
|
22
|
+
class HaveTableMatcher
|
23
|
+
|
24
|
+
attr_accessor :table_name, :repository
|
25
|
+
|
26
|
+
def initialize(table_name)
|
27
|
+
@table_name = table_name
|
28
|
+
end
|
29
|
+
|
30
|
+
def matches?(repository)
|
31
|
+
repository.adapter.storage_exists?(table_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def failure_message
|
35
|
+
%(expected #{repository} to have table '#{table_name}')
|
36
|
+
end
|
37
|
+
|
38
|
+
def negative_failure_message
|
39
|
+
%(expected #{repository} to not have table '#{table_name}')
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
class HaveColumnMatcher
|
45
|
+
|
46
|
+
attr_accessor :table, :column_name
|
47
|
+
|
48
|
+
def initialize(column_name)
|
49
|
+
@column_name = column_name
|
50
|
+
end
|
51
|
+
|
52
|
+
def matches?(table)
|
53
|
+
@table = table
|
54
|
+
table.columns.map { |c| c.name }.include?(column_name.to_s)
|
55
|
+
end
|
56
|
+
|
57
|
+
def failure_message
|
58
|
+
%(expected #{table} to have column '#{column_name}')
|
59
|
+
end
|
60
|
+
|
61
|
+
def negative_failure_message
|
62
|
+
%(expected #{table} to not have column '#{column_name}')
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
class NullableColumnMatcher
|
68
|
+
|
69
|
+
attr_accessor :column
|
70
|
+
|
71
|
+
def matches?(column)
|
72
|
+
@column = column
|
73
|
+
! column.not_null
|
74
|
+
end
|
75
|
+
|
76
|
+
def failure_message
|
77
|
+
%(expected #{column.name} to permit NULL)
|
78
|
+
end
|
79
|
+
|
80
|
+
def negative_failure_message
|
81
|
+
%(expected #{column.name} to be NOT NULL)
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
class PrimaryKeyMatcher
|
87
|
+
|
88
|
+
attr_accessor :column
|
89
|
+
|
90
|
+
def matches?(column)
|
91
|
+
@column = column
|
92
|
+
column.primary_key
|
93
|
+
end
|
94
|
+
|
95
|
+
def failure_message
|
96
|
+
%(expected #{column.name} to be PRIMARY KEY)
|
97
|
+
end
|
98
|
+
|
99
|
+
def negative_failure_message
|
100
|
+
%(expected #{column.name} to not be PRIMARY KEY)
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/sql.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/sql/sqlite3'
|
2
|
+
require File.dirname(__FILE__) + '/sql/mysql'
|
3
|
+
require File.dirname(__FILE__) + '/sql/postgresql'
|
4
|
+
|
5
|
+
module SQL
|
6
|
+
|
7
|
+
class TableCreator
|
8
|
+
attr_accessor :table_name, :opts
|
9
|
+
|
10
|
+
def initialize(adapter, table_name, opts = {}, &block)
|
11
|
+
@adapter = adapter
|
12
|
+
@table_name = table_name.to_s
|
13
|
+
@opts = opts
|
14
|
+
|
15
|
+
@columns = []
|
16
|
+
|
17
|
+
self.instance_eval &block
|
18
|
+
end
|
19
|
+
|
20
|
+
def quoted_table_name
|
21
|
+
@adapter.send(:quote_table_name, table_name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def column(name, type, opts = {})
|
25
|
+
@columns << Column.new(@adapter, name, type, opts)
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_sql
|
29
|
+
"CREATE TABLE #{quoted_table_name} (#{@columns.map{ |c| c.to_sql }.join(', ')})"
|
30
|
+
end
|
31
|
+
|
32
|
+
class Column
|
33
|
+
attr_accessor :name, :type
|
34
|
+
|
35
|
+
def initialize(adapter, name, type, opts = {})
|
36
|
+
@adapter = adapter
|
37
|
+
@name = name.to_s
|
38
|
+
@opts = (opts ||= {})
|
39
|
+
@type = build_type(type)
|
40
|
+
end
|
41
|
+
|
42
|
+
def build_type(type_class)
|
43
|
+
schema = {:name => @name, :quote_column_name => quoted_name}.merge(@opts)
|
44
|
+
schema.merge!(@adapter.class.type_map[type_class])
|
45
|
+
@adapter.property_schema_statement(schema)
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_sql
|
49
|
+
type
|
50
|
+
end
|
51
|
+
|
52
|
+
def quoted_name
|
53
|
+
@adapter.send(:quote_column_name, name)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
class TableModifier
|
60
|
+
attr_accessor :table_name, :opts, :statements, :adapter
|
61
|
+
|
62
|
+
def initialize(adapter, table_name, opts = {}, &block)
|
63
|
+
@adapter = adapter
|
64
|
+
@table_name = table_name.to_s
|
65
|
+
@opts = (opts ||= {})
|
66
|
+
|
67
|
+
@statements = []
|
68
|
+
|
69
|
+
self.instance_eval &block
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_column(name, type, opts = {})
|
73
|
+
column = SQL::TableCreator::Column.new(@adapter, name, type, opts)
|
74
|
+
@statements << "ALTER TABLE #{quoted_table_name} ADD COLUMN #{column.to_sql}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def drop_column(name)
|
78
|
+
# raise NotImplemented for SQLite3. Can't ALTER TABLE, need to copy table.
|
79
|
+
# We'd have to inspect it, and we can't, since we aren't executing any queries yet.
|
80
|
+
# Just write the sql yourself.
|
81
|
+
if name.is_a?(Array)
|
82
|
+
name.each{ |n| drop_column(n) }
|
83
|
+
else
|
84
|
+
@statements << "ALTER TABLE #{quoted_table_name} DROP COLUMN #{quote_column_name(name)}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
alias drop_columns drop_column
|
88
|
+
|
89
|
+
def rename_column(name, new_name, opts = {})
|
90
|
+
# raise NotImplemented for SQLite3
|
91
|
+
@statements << "ALTER TABLE #{quoted_table_name} RENAME COLUMN #{quote_column_name(name)} TO #{quote_column_name(new_name)}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def change_column(name, type, opts = {})
|
95
|
+
# raise NotImplemented for SQLite3
|
96
|
+
@statements << "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(name)} TYPE #{type}"
|
97
|
+
end
|
98
|
+
|
99
|
+
def quote_column_name(name)
|
100
|
+
@adapter.send(:quote_column_name, name.to_s)
|
101
|
+
end
|
102
|
+
|
103
|
+
def quoted_table_name
|
104
|
+
@adapter.send(:quote_table_name, table_name)
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
end
|
data/lib/sql/column.rb
ADDED
data/lib/sql/mysql.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/table'
|
2
|
+
|
3
|
+
module SQL
|
4
|
+
module Mysql
|
5
|
+
|
6
|
+
def supports_schema_transactions?
|
7
|
+
false
|
8
|
+
end
|
9
|
+
|
10
|
+
def table(table_name)
|
11
|
+
SQL::Mysql::Table.new(self, table_name)
|
12
|
+
end
|
13
|
+
|
14
|
+
def recreate_database
|
15
|
+
execute "DROP DATABASE #{db_name}"
|
16
|
+
execute "CREATE DATABASE #{db_name}"
|
17
|
+
execute "USE #{db_name}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def supports_serial?
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
# TODO: move to dm-more/dm-migrations
|
25
|
+
def property_schema_statement(schema)
|
26
|
+
if supports_serial? && schema[:serial]
|
27
|
+
statement = "#{schema[:quote_column_name]} serial PRIMARY KEY"
|
28
|
+
else
|
29
|
+
super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Table
|
34
|
+
def initialize(adapter, table_name)
|
35
|
+
@columns = []
|
36
|
+
adapter.query_table(table_name).each do |col_struct|
|
37
|
+
@columns << SQL::Mysql::Column.new(col_struct)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Column
|
43
|
+
def initialize(col_struct)
|
44
|
+
@name, @type, @default_value, @primary_key = col_struct.name, col_struct.type, col_struct.dflt_value, col_struct.pk
|
45
|
+
|
46
|
+
@not_null = col_struct.notnull == 0
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module SQL
|
2
|
+
module Postgresql
|
3
|
+
|
4
|
+
def supports_schema_transactions?
|
5
|
+
true
|
6
|
+
end
|
7
|
+
|
8
|
+
def table(table_name)
|
9
|
+
SQL::Postgresql::Table.new(self, table_name)
|
10
|
+
end
|
11
|
+
|
12
|
+
def drop_database
|
13
|
+
end
|
14
|
+
|
15
|
+
def recreate_database
|
16
|
+
execute "DROP SCHEMA IF EXISTS test CASCADE"
|
17
|
+
execute "CREATE SCHEMA test"
|
18
|
+
execute "SET search_path TO test"
|
19
|
+
end
|
20
|
+
|
21
|
+
def supports_serial?
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
def property_schema_statement(schema)
|
26
|
+
if supports_serial? && schema[:serial]
|
27
|
+
statement = "#{schema[:quote_column_name]} serial PRIMARY KEY"
|
28
|
+
else
|
29
|
+
statement = super
|
30
|
+
if schema.has_key?(:sequence_name)
|
31
|
+
statement << " DEFAULT nextval('#{schema[:sequence_name]}') NOT NULL"
|
32
|
+
end
|
33
|
+
statement
|
34
|
+
end
|
35
|
+
statement
|
36
|
+
end
|
37
|
+
|
38
|
+
class Table < SQL::Table
|
39
|
+
def initialize(adapter, table_name)
|
40
|
+
@columns = []
|
41
|
+
adapter.query_table(table_name).each do |col_struct|
|
42
|
+
@columns << SQL::Postgresql::Column.new(col_struct)
|
43
|
+
end
|
44
|
+
|
45
|
+
puts "+=+++++++++++++++++++++++++++++++++++++++"
|
46
|
+
# detect column constraints
|
47
|
+
adapter.query(
|
48
|
+
"SELECT * FROM information_schema.table_constraints WHERE table_name='#{table_name}' AND table_schema=current_schema()"
|
49
|
+
).each do |table_constraint|
|
50
|
+
puts table_constraint.inspect
|
51
|
+
adapter.query(
|
52
|
+
"SELECT * FROM information_schema.constraint_column_usage WHERE table_name='#{table_name}' AND table_schema=current_schema()"
|
53
|
+
).each do |constrained_column|
|
54
|
+
@columns.each do |column|
|
55
|
+
if column.name == constrained_column.column_name
|
56
|
+
case table_constraint.constraint_type
|
57
|
+
when "UNIQUE" then column.unique = true
|
58
|
+
when "PRIMARY KEY" then column.primary_key = true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
class Column < SQL::Column
|
69
|
+
def initialize(col_struct)
|
70
|
+
@name, @type, @default_value = col_struct.column_name, col_struct.data_type, col_struct.column_default
|
71
|
+
|
72
|
+
@not_null = col_struct.is_nullable != "YES"
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
data/lib/sql/sqlite3.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/table'
|
2
|
+
|
3
|
+
module SQL
|
4
|
+
module Sqlite3
|
5
|
+
|
6
|
+
def supports_schema_transactions?
|
7
|
+
true
|
8
|
+
end
|
9
|
+
|
10
|
+
def table(table_name)
|
11
|
+
SQL::Table.new(self, table_name)
|
12
|
+
end
|
13
|
+
|
14
|
+
def recreate_database
|
15
|
+
DataMapper.logger.info "Dropping #{@uri.path}"
|
16
|
+
system "rm #{@uri.path}"
|
17
|
+
# do nothing, sqlite will automatically create the database file
|
18
|
+
end
|
19
|
+
|
20
|
+
def supports_serial?
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
# TODO: move to dm-more/dm-migrations
|
25
|
+
def property_schema_statement(schema)
|
26
|
+
statement = super
|
27
|
+
statement << ' PRIMARY KEY AUTOINCREMENT' if supports_serial? && schema[:serial]
|
28
|
+
statement
|
29
|
+
end
|
30
|
+
|
31
|
+
class Table < SQL::Table
|
32
|
+
def initialize(adapter, table_name)
|
33
|
+
@columns = []
|
34
|
+
adapter.query_table(table_name).each do |col_struct|
|
35
|
+
@columns << SQL::Sqlite3::Column.new(col_struct)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Column < SQL::Column
|
41
|
+
def initialize(col_struct)
|
42
|
+
@name, @type, @default_value, @primary_key = col_struct.name, col_struct.type, col_struct.dflt_value, col_struct.pk
|
43
|
+
|
44
|
+
@not_null = col_struct.notnull == 0
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
data/lib/sql/table.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/column'
|
2
|
+
|
3
|
+
module SQL
|
4
|
+
|
5
|
+
class Table
|
6
|
+
|
7
|
+
attr_accessor :name, :columns
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
name
|
11
|
+
end
|
12
|
+
|
13
|
+
def column(column_name)
|
14
|
+
@columns.select { |c| c.name == column_name.to_s }.first
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require Pathname(__FILE__).dirname.expand_path.parent + 'spec_helper'
|
3
|
+
|
4
|
+
if HAS_SQLITE3 || HAS_MYSQL || HAS_POSTGRES
|
5
|
+
describe 'empty migration runner' do
|
6
|
+
it "should return an empty array if no migrations have been defined" do
|
7
|
+
migrations.should be_kind_of(Array)
|
8
|
+
migrations.should have(0).item
|
9
|
+
end
|
10
|
+
end
|
11
|
+
describe 'migration runnner' do
|
12
|
+
# set up some 'global' setup and teardown tasks
|
13
|
+
before(:each) do
|
14
|
+
migration( 1, :create_people_table) { }
|
15
|
+
end
|
16
|
+
|
17
|
+
after(:each) do
|
18
|
+
@@migrations = []
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#migration' do
|
22
|
+
|
23
|
+
it 'should create a new migration object, and add it to the list of migrations' do
|
24
|
+
@@migrations.should be_kind_of(Array)
|
25
|
+
@@migrations.should have(1).item
|
26
|
+
@@migrations.first.name.should == "create_people_table"
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should allow multiple migrations to be added' do
|
30
|
+
migration( 2, :add_dob_to_people) { }
|
31
|
+
migration( 2, :add_favorite_pet_to_people) { }
|
32
|
+
migration( 3, :add_something_else_to_people) { }
|
33
|
+
@@migrations.should have(4).items
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should raise an error on adding with a duplicated name' do
|
37
|
+
lambda { migration( 1, :create_people_table) { } }.should raise_error(RuntimeError, /Migration name conflict/)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#migrate_up! and #migrate_down!' do
|
43
|
+
before(:each) do
|
44
|
+
migration( 2, :add_dob_to_people) { }
|
45
|
+
migration( 2, :add_favorite_pet_to_people) { }
|
46
|
+
migration( 3, :add_something_else_to_people) { }
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'calling migrate_up! should migrate up all the migrations' do
|
50
|
+
# add our expection that migrate_up should be called
|
51
|
+
@@migrations.each do |m|
|
52
|
+
m.should_receive(:perform_up)
|
53
|
+
end
|
54
|
+
migrate_up!
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'calling migrate_up! with an arguement should only migrate to that level' do
|
58
|
+
@@migrations.each do |m|
|
59
|
+
if m.position <= 2
|
60
|
+
m.should_receive(:perform_up)
|
61
|
+
else
|
62
|
+
m.should_not_receive(:perform_up)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
migrate_up!(2)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'calling migrate_down! should migrate down all the migrations' do
|
69
|
+
# add our expection that migrate_up should be called
|
70
|
+
@@migrations.each do |m|
|
71
|
+
m.should_receive(:perform_down)
|
72
|
+
end
|
73
|
+
migrate_down!
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|