wineskins 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module Wineskins
2
+ VERSION = '0.2.2'
3
+ end
@@ -0,0 +1,3 @@
1
+ ['wineskins', 'wineskins/runner'].each do |rb|
2
+ require File.expand_path(rb, File.dirname(__FILE__))
3
+ end
@@ -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