wineskins 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +9 -0
- data/LICENSE.txt +18 -0
- data/README.markdown +235 -0
- data/Rakefile +7 -0
- data/bin/wskins +5 -0
- data/lib/wineskins.rb +269 -0
- data/lib/wineskins/record_methods.rb +25 -0
- data/lib/wineskins/runner.rb +132 -0
- data/lib/wineskins/schema_methods.rb +100 -0
- data/lib/wineskins/transcript.rb +24 -0
- data/lib/wineskins/utils.rb +19 -0
- data/lib/wineskins/version.rb +3 -0
- data/lib/wineskins_cli.rb +3 -0
- data/test/helper.rb +128 -0
- data/test/helpers/transfer_assertions.rb +102 -0
- data/test/suite.rb +3 -0
- data/test/test_transfer_callbacks.rb +52 -0
- data/test/test_transfer_run_table.rb +172 -0
- data/test/test_transfer_run_tables.rb +131 -0
- data/todo.yml +10 -0
- data/wineskins.gemspec +25 -0
- metadata +98 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
module Wineskins
|
2
|
+
|
3
|
+
module RecordMethods
|
4
|
+
|
5
|
+
# reads + inserts ten records at a time
|
6
|
+
def transfer_records(table)
|
7
|
+
src_tbl, dst_tbl = table.source_name, table.dest_name
|
8
|
+
rename = table.rename_map(source[src_tbl].columns)
|
9
|
+
|
10
|
+
set_progressbar dst_tbl, source[src_tbl].count
|
11
|
+
|
12
|
+
source[src_tbl].each_slice(10) do |recs|
|
13
|
+
dest[dst_tbl].multi_insert(
|
14
|
+
recs.map {|rec|
|
15
|
+
remap = Utils.remap_hash(rec, rename)
|
16
|
+
block_given? ? yield(remap) : remap
|
17
|
+
}
|
18
|
+
)
|
19
|
+
progressbar.inc(10) if progressbar
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Wineskins
|
4
|
+
|
5
|
+
class Runner
|
6
|
+
|
7
|
+
def initialize(argv=nil)
|
8
|
+
@config = Config.new
|
9
|
+
parse_opts argv
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse_opts(argv)
|
13
|
+
opts = OptionParser.new do |opts|
|
14
|
+
opts.default_argv = argv if argv
|
15
|
+
|
16
|
+
opts.banner = "Usage: \n"+
|
17
|
+
"(1) wskins [options] [source-db] [dest-db]\n" +
|
18
|
+
"(2) wskins [options]"
|
19
|
+
|
20
|
+
opts.separator nil
|
21
|
+
opts.separator "Options:"
|
22
|
+
|
23
|
+
opts.on "-c", "--config FILE", "Transfer script, default #{@config.script}" do |file|
|
24
|
+
@config.script = file
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on "-r", "--require FILE", "Require ruby file or gem before running" do |file|
|
28
|
+
@config.requires << file
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on "--[no-]dry-run", "Don't commit changes" do |bool|
|
32
|
+
@config.dry_run = bool
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on "-h", "--help", "Show this message" do
|
36
|
+
puts opts
|
37
|
+
exit
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.separator nil
|
41
|
+
opts.separator "Summary:"
|
42
|
+
opts.separator wrap( %Q[
|
43
|
+
Transfer schema and data from [source-db] to [dest-db], according to
|
44
|
+
instructions in transfer script (by default, #{@config.script}).
|
45
|
+
|
46
|
+
In form (1), specify a source and destination database URL (as recognized by
|
47
|
+
Sequel.connect). In form (2), require (-r) a ruby program that connects to
|
48
|
+
databases manually and assigns SOURCE_DB= and DEST_DB=
|
49
|
+
|
50
|
+
], 80)
|
51
|
+
end
|
52
|
+
|
53
|
+
opts.parse!(argv)
|
54
|
+
@config.source = argv.shift
|
55
|
+
@config.dest = argv.shift
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
def run
|
60
|
+
require_all
|
61
|
+
set_global_unless_defined 'SOURCE_DB', @config.source_db
|
62
|
+
set_global_unless_defined 'DEST_DB', @config.dest_db
|
63
|
+
validate
|
64
|
+
t = Transfer.new(::SOURCE_DB, ::DEST_DB)
|
65
|
+
eval "t.define {\n" + @config.script_text + "\n}"
|
66
|
+
t.run @config.run_options
|
67
|
+
end
|
68
|
+
|
69
|
+
# require specified libs
|
70
|
+
def require_all
|
71
|
+
@config.requires.each do |r| require r end
|
72
|
+
end
|
73
|
+
|
74
|
+
def set_global_unless_defined(const, value)
|
75
|
+
unless Object.const_defined?(const)
|
76
|
+
Object.const_set(const, value)
|
77
|
+
end
|
78
|
+
Object.const_get(const)
|
79
|
+
end
|
80
|
+
|
81
|
+
def validate
|
82
|
+
unless ::SOURCE_DB && ::DEST_DB
|
83
|
+
$stderr.puts wrap( %Q[
|
84
|
+
You must specify a source and destination database URL, or manually assign
|
85
|
+
SOURCE_DB= and DEST_DB=
|
86
|
+
], 80)
|
87
|
+
exit false
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def wrap(text, width) # :doc:
|
94
|
+
text.gsub(/(.{1,#{width}})( +|$\n?)|(.{1,#{width}})/, "\\1\\3\n")
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
class Config < Struct.new(:source, :dest, :script, :dry_run)
|
100
|
+
|
101
|
+
attr_reader :requires
|
102
|
+
def initialize(*args)
|
103
|
+
@requires = []
|
104
|
+
super
|
105
|
+
self.script ||= "./transfer.rb"
|
106
|
+
self.dry_run ||= false
|
107
|
+
end
|
108
|
+
|
109
|
+
def source_db
|
110
|
+
return unless self.source
|
111
|
+
@source_db ||= Sequel.connect(self.source)
|
112
|
+
end
|
113
|
+
|
114
|
+
def dest_db
|
115
|
+
return unless self.dest
|
116
|
+
@dest_db ||= Sequel.connect(self.dest)
|
117
|
+
end
|
118
|
+
|
119
|
+
def script_text
|
120
|
+
::File.read(self.script)
|
121
|
+
end
|
122
|
+
|
123
|
+
def run_options
|
124
|
+
[:dry_run].inject({}) do |memo, opt|
|
125
|
+
memo[opt] = self.send(opt)
|
126
|
+
memo
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Wineskins
|
2
|
+
|
3
|
+
module SchemaMethods
|
4
|
+
|
5
|
+
def transfer_table(table)
|
6
|
+
src_tbl, dst_tbl = table.source_name, table.dest_name
|
7
|
+
rename = table.rename_map(source[src_tbl].columns)
|
8
|
+
alters = table.dest_columns
|
9
|
+
this = self
|
10
|
+
dest.create_table(dst_tbl) do
|
11
|
+
this.source_schema(src_tbl).each do |(fld, spec)|
|
12
|
+
if args = alters[fld]
|
13
|
+
column rename[fld], *args
|
14
|
+
else
|
15
|
+
column_opts = this.schema_to_column_options(spec)
|
16
|
+
column rename[fld], column_opts.delete(:type), column_opts
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def transfer_indexes(table)
|
23
|
+
src_tbl, dst_tbl = table.source_name, table.dest_name
|
24
|
+
rename = table.rename_map(source[src_tbl].columns)
|
25
|
+
this = self
|
26
|
+
dest.alter_table(dst_tbl) do
|
27
|
+
this.source_indexes(src_tbl).each do |(name, spec)|
|
28
|
+
index_opts = this.schema_to_index_options(spec)
|
29
|
+
index_cols = spec[:columns].map {|c| rename[c]}
|
30
|
+
add_index index_cols, index_opts
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def transfer_fk_constraints(table, table_rename={})
|
36
|
+
src_tbl, dst_tbl = table.source_name, table.dest_name
|
37
|
+
rename = table.rename_map(source[src_tbl].columns)
|
38
|
+
this = self
|
39
|
+
dest.alter_table(dst_tbl) do
|
40
|
+
this.source_foreign_key_list(src_tbl).each do |spec|
|
41
|
+
fk_opts = this.schema_to_foreign_key_options(spec)
|
42
|
+
fk_cols = spec[:columns].map {|c| rename[c]}
|
43
|
+
add_foreign_key fk_cols, table_rename[spec[:table]] || spec[:table], fk_opts
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def source_schema(tbl,opts={})
|
49
|
+
source.schema(tbl,opts)
|
50
|
+
rescue Sequel::Error
|
51
|
+
warn "Source database does not expose schema metadata. " +
|
52
|
+
"You should define schema manually."
|
53
|
+
[]
|
54
|
+
end
|
55
|
+
|
56
|
+
def source_indexes(tbl,opts={})
|
57
|
+
source.indexes(tbl,opts)
|
58
|
+
rescue Sequel::Error
|
59
|
+
warn "Source database does not expose index metadata. " +
|
60
|
+
"You should define indexes manually."
|
61
|
+
{}
|
62
|
+
end
|
63
|
+
|
64
|
+
def source_foreign_key_list(tbl,opts={})
|
65
|
+
source.foreign_key_list(tbl,opts)
|
66
|
+
rescue Sequel::Error
|
67
|
+
warn "Source database does not expose foreign key metadata. " +
|
68
|
+
"You should define foreign key constraints manually."
|
69
|
+
[]
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
def schema_to_column_options(spec)
|
74
|
+
Utils.remap_hash(
|
75
|
+
Utils.limit_hash(spec, [
|
76
|
+
:primary_key,
|
77
|
+
:default,
|
78
|
+
:allow_null
|
79
|
+
]),
|
80
|
+
:allow_null => :null
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
def schema_to_index_options(spec)
|
85
|
+
Utils.limit_hash spec, [:unique]
|
86
|
+
end
|
87
|
+
|
88
|
+
def schema_to_foreign_key_options(spec)
|
89
|
+
Utils.limit_hash spec, [
|
90
|
+
:key,
|
91
|
+
:deferrable,
|
92
|
+
:name,
|
93
|
+
:on_delete,
|
94
|
+
:on_update
|
95
|
+
]
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Wineskins
|
2
|
+
|
3
|
+
class Transcript
|
4
|
+
|
5
|
+
def initialize(io=nil)
|
6
|
+
@io = io || $stdout
|
7
|
+
end
|
8
|
+
|
9
|
+
# write all sql and errors, stripping the duration from the front
|
10
|
+
def method_missing(m, msg)
|
11
|
+
write msg.gsub(/\A\([\d\.s]+\)\s+/,'')
|
12
|
+
end
|
13
|
+
|
14
|
+
def write(msg)
|
15
|
+
if String === @io
|
16
|
+
File.open(@io, 'w+') {|f| f.puts msg}
|
17
|
+
else
|
18
|
+
@io.puts msg
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Wineskins
|
2
|
+
|
3
|
+
module Utils
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def remap_hash(hash, map)
|
7
|
+
hash.inject({}) do |memo, (k,v)|
|
8
|
+
memo[ map[k] || k ] = v
|
9
|
+
memo
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def limit_hash(hash, keys)
|
14
|
+
Hash[ hash.select {|k,v| keys.include?(k)} ]
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
gem 'minitest'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
|
4
|
+
class SequelSpec < MiniTest::Spec
|
5
|
+
def run(*args, &block)
|
6
|
+
TEST_DB.dest.transaction(:rollback => :always){super}
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
MiniTest::Spec.register_spec_type(/functional/i, SequelSpec)
|
11
|
+
|
12
|
+
|
13
|
+
require 'logger'
|
14
|
+
|
15
|
+
class << (TEST_DB = Object.new)
|
16
|
+
|
17
|
+
attr_accessor :source_connect, :dest_connect
|
18
|
+
|
19
|
+
def source
|
20
|
+
@source ||= Sequel.connect(source_connect)
|
21
|
+
end
|
22
|
+
|
23
|
+
def dest
|
24
|
+
@dest ||= Sequel.connect(dest_connect)
|
25
|
+
end
|
26
|
+
|
27
|
+
def setup_source(*tables)
|
28
|
+
source.loggers = [::Logger.new('test/log/source.log')]
|
29
|
+
setup source, *tables
|
30
|
+
end
|
31
|
+
|
32
|
+
def setup_dest(*tables)
|
33
|
+
dest.loggers = [::Logger.new('test/log/dest.log')]
|
34
|
+
setup dest, *tables
|
35
|
+
end
|
36
|
+
|
37
|
+
def setup(db, *tables)
|
38
|
+
tables.each do |t|
|
39
|
+
TEST_SCHEMA.send(t, db)
|
40
|
+
db[t].multi_insert(
|
41
|
+
TEST_DATA[t]
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
class << (TEST_SCHEMA = Object.new)
|
49
|
+
|
50
|
+
def tests(db)
|
51
|
+
db.create_table! :tests do
|
52
|
+
primary_key :id
|
53
|
+
column :name, :string
|
54
|
+
column :score, :integer, :index => true
|
55
|
+
column :taken_at, :datetime
|
56
|
+
index :name, :unique => true
|
57
|
+
foreign_key :user_id, :users, :on_delete => :cascade
|
58
|
+
foreign_key :cat_id, :test_categories, :on_update => :cascade
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def users(db)
|
63
|
+
db.create_table! :users do
|
64
|
+
primary_key :uid
|
65
|
+
column :username, :string
|
66
|
+
column :joined_at, :datetime
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_categories(db)
|
71
|
+
db.create_table! :test_categories do
|
72
|
+
primary_key :id
|
73
|
+
column :name, :string
|
74
|
+
column :desc, :string
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
TEST_DATA = {
|
81
|
+
|
82
|
+
:tests => [
|
83
|
+
{:id => 1, :name => 'test1', :score => 65, :taken_at => Time.now - rand(60*60*24),
|
84
|
+
:user_id => 11, :cat_id => 1},
|
85
|
+
{:id => 2, :name => 'test2', :score => 21, :taken_at => Time.now - rand(60*60*24),
|
86
|
+
:user_id => 10, :cat_id => 1},
|
87
|
+
{:id => 3, :name => 'test3', :score => 75, :taken_at => Time.now - rand(60*60*24),
|
88
|
+
:user_id => 9, :cat_id => 1},
|
89
|
+
{:id => 4, :name => 'test4', :score => 84, :taken_at => Time.now - rand(60*60*24),
|
90
|
+
:user_id => 8, :cat_id => 2},
|
91
|
+
{:id => 5, :name => 'test5', :score => 60, :taken_at => Time.now - rand(60*60*24),
|
92
|
+
:user_id => 7, :cat_id => 3},
|
93
|
+
{:id => 6, :name => 'test6', :score => 20, :taken_at => Time.now - rand(60*60*24),
|
94
|
+
:user_id => 1, :cat_id => 1},
|
95
|
+
{:id => 7, :name => 'test7', :score => 66, :taken_at => Time.now - rand(60*60*24),
|
96
|
+
:user_id => 1, :cat_id => 2},
|
97
|
+
{:id => 8, :name => 'test8', :score => 87, :taken_at => Time.now - rand(60*60*24),
|
98
|
+
:user_id => 2, :cat_id => 2},
|
99
|
+
{:id => 9, :name => 'test9', :score => 72, :taken_at => Time.now - rand(60*60*24),
|
100
|
+
:user_id => 4, :cat_id => 3},
|
101
|
+
{:id => 10, :name => 'test10', :score => 33, :taken_at => Time.now - rand(60*60*24),
|
102
|
+
:user_id => 5, :cat_id => 1},
|
103
|
+
{:id => 11, :name => 'test11', :score => 99, :taken_at => Time.now - rand(60*60*24),
|
104
|
+
:user_id => 8, :cat_id => 1}
|
105
|
+
],
|
106
|
+
|
107
|
+
:users => [
|
108
|
+
{:uid => 1, :username => "Wilson", :joined_at => Time.now - rand(60*60*24)},
|
109
|
+
{:uid => 2, :username => "Abner", :joined_at => Time.now - rand(60*60*24)},
|
110
|
+
{:uid => 3, :username => "Penelope", :joined_at => Time.now - rand(60*60*24)},
|
111
|
+
{:uid => 4, :username => "Harrison", :joined_at => Time.now - rand(60*60*24)},
|
112
|
+
{:uid => 5, :username => "Luke", :joined_at => Time.now - rand(60*60*24)},
|
113
|
+
{:uid => 6, :username => "Kramer", :joined_at => Time.now - rand(60*60*24)},
|
114
|
+
{:uid => 7, :username => "Gjert", :joined_at => Time.now - rand(60*60*24)},
|
115
|
+
{:uid => 8, :username => "Yvette", :joined_at => Time.now - rand(60*60*24)},
|
116
|
+
{:uid => 9, :username => "Ruth", :joined_at => Time.now - rand(60*60*24)},
|
117
|
+
{:uid => 10, :username => "Lambert", :joined_at => Time.now - rand(60*60*24)},
|
118
|
+
{:uid => 11, :username => "Percy", :joined_at => Time.now - rand(60*60*24)}
|
119
|
+
],
|
120
|
+
|
121
|
+
:test_categories => [
|
122
|
+
{:id => 1, :name => 'first', :desc => 'first test'},
|
123
|
+
{:id => 2, :name => 'second', :desc => 'second test'},
|
124
|
+
{:id => 3, :name => 'third'}
|
125
|
+
]
|
126
|
+
|
127
|
+
}
|
128
|
+
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module TransferAssertions
|
2
|
+
|
3
|
+
# I wish we could compare :type as well, but this seems somewhat adapter-
|
4
|
+
# dependent. Probably :default is adapter-dependent too.
|
5
|
+
#
|
6
|
+
def assert_columns_match(table)
|
7
|
+
match_keys = [:allow_null, :primary_key, :default]
|
8
|
+
exp = source.schema(table).map {|name, col|
|
9
|
+
[name, Wineskins::Utils.limit_hash(col, match_keys)]
|
10
|
+
}
|
11
|
+
act = dest.schema(table).map {|name, col|
|
12
|
+
[name, Wineskins::Utils.limit_hash(col, match_keys)]
|
13
|
+
}
|
14
|
+
assert_equal exp.length, act.length,
|
15
|
+
"Expected #{exp.length} columns in table #{table}, got #{act.length}"
|
16
|
+
exp.each do |col|
|
17
|
+
assert_includes act, col,
|
18
|
+
"Unexpected column options for #{col[0]} in table #{table}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def assert_column_matches(table, col, specs=nil)
|
23
|
+
src_col, dst_col = Array(col)
|
24
|
+
dst_col ||= src_col
|
25
|
+
exp = if specs
|
26
|
+
[src_col, specs]
|
27
|
+
else
|
28
|
+
source.schema(table).find {|name,col| name == src_col}
|
29
|
+
end
|
30
|
+
act = dest.schema(table).find {|name,col| name == dst_col}
|
31
|
+
refute_nil act, "Expected column #{dst_col} not found in table #{table}"
|
32
|
+
|
33
|
+
match_keys = [:allow_null, :primary_key, :default]
|
34
|
+
exp = [exp[0], Wineskins::Utils.limit_hash( exp[1], match_keys )]
|
35
|
+
act = [act[0], Wineskins::Utils.limit_hash( act[1], match_keys )]
|
36
|
+
assert_equal exp[1], act[1],
|
37
|
+
"Unexpected column options for #{dst_col} in table #{table}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def assert_indexes_match(table)
|
41
|
+
exp, act = source.indexes(table), dest.indexes(table)
|
42
|
+
assert_equal exp.length, act.length,
|
43
|
+
"Expected #{exp.length} indexes for table #{table}, got #{act.length}"
|
44
|
+
exp.values.each do |idx|
|
45
|
+
assert_includes act.values, idx,
|
46
|
+
"Unexpected index options in table #{table}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def assert_index_matches(table, cols, rename={})
|
51
|
+
src_cols = Array(cols)
|
52
|
+
dst_cols = src_cols.map {|c| rename[c] || c}
|
53
|
+
exp = source.indexes(table).find {|name, specs|
|
54
|
+
specs[:columns] == src_cols
|
55
|
+
}
|
56
|
+
act = dest.indexes(table).find {|name, specs|
|
57
|
+
specs[:columns] == dst_cols
|
58
|
+
}
|
59
|
+
refute_nil act, "Expected index #{dst_cols.inspect} not found in table #{table}"
|
60
|
+
|
61
|
+
match_keys = [:unique]
|
62
|
+
exp = Wineskins::Utils.limit_hash(exp[1], match_keys)
|
63
|
+
act = Wineskins::Utils.limit_hash(act[1], match_keys)
|
64
|
+
assert_equal exp, act,
|
65
|
+
"Unexpected index options for #{dst_cols.inspect} in table #{table}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def assert_fk_match(table)
|
69
|
+
match_keys = [:columns, :table, :key, :on_delete, :on_update]
|
70
|
+
exp = source.foreign_key_list(table).map {|fk|
|
71
|
+
Wineskins::Utils.limit_hash(fk, match_keys)
|
72
|
+
}
|
73
|
+
act = dest.foreign_key_list(table).map {|fk|
|
74
|
+
Wineskins::Utils.limit_hash(fk, match_keys)
|
75
|
+
}
|
76
|
+
assert_equal exp.length, act.length,
|
77
|
+
"Expected #{exp.length} foreign key constraints for table #{table}, got #{act.length}"
|
78
|
+
exp.each do |fk|
|
79
|
+
assert_includes act, fk,
|
80
|
+
"Unexpected foreign key options in table #{table}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def assert_fk_matches(table, cols, rename={})
|
85
|
+
src_cols = Array(cols)
|
86
|
+
dst_cols = src_cols.map {|c| rename[c] || c}
|
87
|
+
exp = source.foreign_key_list(table).find {|specs|
|
88
|
+
specs[:columns] == src_cols
|
89
|
+
}
|
90
|
+
act = dest.foreign_key_list(table).find {|specs|
|
91
|
+
specs[:columns] == dst_cols
|
92
|
+
}
|
93
|
+
refute_nil act, "Expected foreign key #{dst_cols.inspect} not found in table #{table}"
|
94
|
+
|
95
|
+
match_keys = [:table, :key, :on_delete, :on_update]
|
96
|
+
exp = Wineskins::Utils.limit_hash(exp, match_keys)
|
97
|
+
act = Wineskins::Utils.limit_hash(act, match_keys)
|
98
|
+
assert_equal exp, act,
|
99
|
+
"Unexpected foreign key options for #{dst_cols.inspect} in table #{table}"
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|