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.
@@ -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