wineskins 0.2.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/.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
|