xmysql2psql 0.2.0
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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +123 -0
- data/Rakefile +79 -0
- data/bin/xmysql2psql +7 -0
- data/lib/xmysql2psql/config.rb +100 -0
- data/lib/xmysql2psql/config_base.rb +39 -0
- data/lib/xmysql2psql/converter.rb +55 -0
- data/lib/xmysql2psql/errors.rb +16 -0
- data/lib/xmysql2psql/mysql_reader.rb +190 -0
- data/lib/xmysql2psql/postgres_db_writer.rb +183 -0
- data/lib/xmysql2psql/postgres_file_writer.rb +146 -0
- data/lib/xmysql2psql/postgres_writer.rb +154 -0
- data/lib/xmysql2psql/version.rb +9 -0
- data/lib/xmysql2psql/writer.rb +6 -0
- data/lib/xmysql2psql.rb +41 -0
- data/test/fixtures/config_all_options.yml +38 -0
- data/test/fixtures/seed_integration_tests.sql +24 -0
- data/test/integration/convert_to_db_test.rb +29 -0
- data/test/integration/convert_to_file_test.rb +66 -0
- data/test/integration/converter_test.rb +34 -0
- data/test/integration/mysql_reader_base_test.rb +35 -0
- data/test/integration/mysql_reader_test.rb +47 -0
- data/test/integration/postgres_db_writer_base_test.rb +30 -0
- data/test/lib/ext_test_unit.rb +30 -0
- data/test/lib/test_helper.rb +88 -0
- data/test/units/config_base_test.rb +49 -0
- data/test/units/config_test.rb +31 -0
- data/test/units/postgres_file_writer_test.rb +29 -0
- data/xmysql2psql.gemspec +79 -0
- metadata +144 -0
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'pg'
|
2
|
+
|
3
|
+
require 'xmysql2psql/postgres_writer'
|
4
|
+
|
5
|
+
class Xmysql2psql
|
6
|
+
|
7
|
+
class PostgresDbWriter < PostgresWriter
|
8
|
+
attr_reader :conn, :hostname, :login, :password, :database, :schema, :port
|
9
|
+
|
10
|
+
def db_writer?
|
11
|
+
true
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(options)
|
15
|
+
@hostname, @login, @password, @database, @port =
|
16
|
+
options.pghostname('localhost'), options.pgusername,
|
17
|
+
options.pgpassword, options.pgdatabase, options.pgport(5432).to_s
|
18
|
+
@database, @schema = database.split(":")
|
19
|
+
open
|
20
|
+
end
|
21
|
+
|
22
|
+
def open
|
23
|
+
@conn = PGconn.new(hostname, port, '', '', database, login, password)
|
24
|
+
@conn.exec("SET search_path TO #{PGconn.quote_ident(schema)}") if schema
|
25
|
+
@conn.exec("SET client_encoding = 'UTF8'")
|
26
|
+
@conn.exec("SET standard_conforming_strings = off") if @conn.server_version >= 80200
|
27
|
+
@conn.exec("SET check_function_bodies = false")
|
28
|
+
@conn.exec("SET client_min_messages = warning")
|
29
|
+
end
|
30
|
+
|
31
|
+
def close
|
32
|
+
@conn.close
|
33
|
+
end
|
34
|
+
|
35
|
+
def exists?(relname)
|
36
|
+
rc = @conn.exec("SELECT COUNT(*) FROM pg_class WHERE relname = '#{relname}'")
|
37
|
+
(!rc.nil?) && (rc.to_a.length==1) && (rc.first.count.to_i==1)
|
38
|
+
end
|
39
|
+
|
40
|
+
def write_table(table)
|
41
|
+
puts "Creating table #{table.name}..."
|
42
|
+
primary_keys = []
|
43
|
+
serial_key = nil
|
44
|
+
maxval = nil
|
45
|
+
|
46
|
+
columns = table.columns.map do |column|
|
47
|
+
if column[:auto_increment]
|
48
|
+
serial_key = column[:name]
|
49
|
+
maxval = column[:maxval].to_i < 1 ? 1 : column[:maxval] + 1
|
50
|
+
end
|
51
|
+
if column[:primary_key]
|
52
|
+
primary_keys << column[:name]
|
53
|
+
end
|
54
|
+
" " + column_description(column)
|
55
|
+
end.join(",\n")
|
56
|
+
|
57
|
+
if serial_key
|
58
|
+
if @conn.server_version < 80200
|
59
|
+
serial_key_seq = "#{table.name}_#{serial_key}_seq"
|
60
|
+
@conn.exec("DROP SEQUENCE #{serial_key_seq} CASCADE") if exists?(serial_key_seq)
|
61
|
+
else
|
62
|
+
@conn.exec("DROP SEQUENCE IF EXISTS #{table.name}_#{serial_key}_seq CASCADE")
|
63
|
+
end
|
64
|
+
@conn.exec <<-EOF
|
65
|
+
CREATE SEQUENCE #{table.name}_#{serial_key}_seq
|
66
|
+
INCREMENT BY 1
|
67
|
+
NO MAXVALUE
|
68
|
+
NO MINVALUE
|
69
|
+
CACHE 1
|
70
|
+
EOF
|
71
|
+
|
72
|
+
@conn.exec "SELECT pg_catalog.setval('#{table.name}_#{serial_key}_seq', #{maxval}, true)"
|
73
|
+
end
|
74
|
+
|
75
|
+
if @conn.server_version < 80200
|
76
|
+
@conn.exec "DROP TABLE #{PGconn.quote_ident(table.name)} CASCADE;" if exists?(table.name)
|
77
|
+
else
|
78
|
+
@conn.exec "DROP TABLE IF EXISTS #{PGconn.quote_ident(table.name)} CASCADE;"
|
79
|
+
end
|
80
|
+
create_sql = "CREATE TABLE #{PGconn.quote_ident(table.name)} (\n" + columns + "\n)\nWITHOUT OIDS;"
|
81
|
+
begin
|
82
|
+
@conn.exec(create_sql)
|
83
|
+
rescue Exception => e
|
84
|
+
puts "Error: \n#{create_sql}"
|
85
|
+
raise
|
86
|
+
end
|
87
|
+
puts "Created table #{table.name}"
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
def write_indexes(table)
|
92
|
+
puts "Indexing table #{table.name}..."
|
93
|
+
if primary_index = table.indexes.find {|index| index[:primary]}
|
94
|
+
@conn.exec("ALTER TABLE #{PGconn.quote_ident(table.name)} ADD CONSTRAINT \"#{table.name}_pkey\" PRIMARY KEY(#{primary_index[:columns].map {|col| PGconn.quote_ident(col)}.join(", ")})")
|
95
|
+
end
|
96
|
+
|
97
|
+
table.indexes.each do |index|
|
98
|
+
next if index[:primary]
|
99
|
+
unique = index[:unique] ? "UNIQUE " : nil
|
100
|
+
|
101
|
+
#MySQL allows an index name which could be equal to a table name, Postgres doesn't
|
102
|
+
indexname = index[:name]
|
103
|
+
if indexname.eql?(table.name)
|
104
|
+
indexnamenew = "#{indexname}_index"
|
105
|
+
puts "WARNING: index \"#{indexname}\" equals table name. This is not allowed by postgres and will be renamed to \"#{indexnamenew}\""
|
106
|
+
indexname = indexnamenew
|
107
|
+
end
|
108
|
+
|
109
|
+
if @conn.server_version < 80200
|
110
|
+
@conn.exec("DROP INDEX #{PGconn.quote_ident(indexname)} CASCADE;") if exists?(indexname)
|
111
|
+
else
|
112
|
+
@conn.exec("DROP INDEX IF EXISTS #{PGconn.quote_ident(indexname)} CASCADE;")
|
113
|
+
end
|
114
|
+
@conn.exec("CREATE #{unique}INDEX #{PGconn.quote_ident(indexname)} ON #{PGconn.quote_ident(table.name)} (#{index[:columns].map {|col| PGconn.quote_ident(col)}.join(", ")});")
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
#@conn.exec("VACUUM FULL ANALYZE #{PGconn.quote_ident(table.name)}")
|
119
|
+
puts "Indexed table #{table.name}"
|
120
|
+
rescue Exception => e
|
121
|
+
puts "Couldn't create indexes on #{table} (#{table.indexes.inspect})"
|
122
|
+
puts e
|
123
|
+
puts e.backtrace[0,3].join("\n")
|
124
|
+
end
|
125
|
+
|
126
|
+
def write_constraints(table)
|
127
|
+
table.foreign_keys.each do |key|
|
128
|
+
key_sql = "ALTER TABLE #{PGconn.quote_ident(table.name)} ADD FOREIGN KEY (#{PGconn.quote_ident(key[:column])}) REFERENCES #{PGconn.quote_ident(key[:ref_table])}(#{PGconn.quote_ident(key[:ref_column])})"
|
129
|
+
begin
|
130
|
+
@conn.exec(key_sql)
|
131
|
+
rescue Exception => e
|
132
|
+
puts "Error: \n#{key_sql}\n#{e}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def format_eta (t)
|
138
|
+
t = t.to_i
|
139
|
+
sec = t % 60
|
140
|
+
min = (t / 60) % 60
|
141
|
+
hour = t / 3600
|
142
|
+
sprintf("%02dh:%02dm:%02ds", hour, min, sec)
|
143
|
+
end
|
144
|
+
|
145
|
+
def write_contents(table, reader)
|
146
|
+
_time1 = Time.now
|
147
|
+
copy_line = "COPY #{PGconn.quote_ident(table.name)} (#{table.columns.map {|column| PGconn.quote_ident(column[:name])}.join(", ")}) FROM stdin;"
|
148
|
+
@conn.exec(copy_line)
|
149
|
+
puts "Counting rows of #{table.name}... "
|
150
|
+
STDOUT.flush
|
151
|
+
rowcount = table.count_rows
|
152
|
+
puts "Rows counted"
|
153
|
+
puts "Loading #{table.name}..."
|
154
|
+
STDOUT.flush
|
155
|
+
_counter = reader.paginated_read(table, 1000) do |row, counter|
|
156
|
+
line = []
|
157
|
+
process_row(table, row)
|
158
|
+
@conn.put_copy_data(row.join("\t") + "\n")
|
159
|
+
|
160
|
+
if counter != 0 && counter % 20000 == 0
|
161
|
+
elapsedTime = Time.now - _time1
|
162
|
+
eta = elapsedTime * rowcount / counter - elapsedTime
|
163
|
+
etaf = self.format_eta(eta)
|
164
|
+
etatimef = (Time.now + eta).strftime("%Y/%m/%d %H:%M")
|
165
|
+
printf "\r#{counter} of #{rowcount} rows loaded. [ETA: #{etatimef} (#{etaf})]"
|
166
|
+
STDOUT.flush
|
167
|
+
end
|
168
|
+
|
169
|
+
if counter % 5000 == 0
|
170
|
+
@conn.put_copy_end
|
171
|
+
@conn.exec(copy_line)
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
_time2 = Time.now
|
176
|
+
puts "\n#{_counter} rows loaded in #{((_time2 - _time1) / 60).round}min #{((_time2 - _time1) % 60).round}s"
|
177
|
+
# @conn.putline(".\n")
|
178
|
+
@conn.put_copy_end
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'xmysql2psql/postgres_writer'
|
2
|
+
|
3
|
+
class Xmysql2psql
|
4
|
+
|
5
|
+
class PostgresFileWriter < PostgresWriter
|
6
|
+
def initialize(file)
|
7
|
+
@f = File.open(file, "w+")
|
8
|
+
@f << <<-EOF
|
9
|
+
-- MySQL 2 PostgreSQL dump\n
|
10
|
+
SET client_encoding = 'UTF8';
|
11
|
+
SET standard_conforming_strings = off;
|
12
|
+
SET check_function_bodies = false;
|
13
|
+
SET client_min_messages = warning;
|
14
|
+
|
15
|
+
EOF
|
16
|
+
end
|
17
|
+
|
18
|
+
def db_writer?
|
19
|
+
false
|
20
|
+
end
|
21
|
+
|
22
|
+
def truncate(table)
|
23
|
+
serial_key = nil
|
24
|
+
maxval = nil
|
25
|
+
|
26
|
+
table.columns.map do |column|
|
27
|
+
if column[:auto_increment]
|
28
|
+
serial_key = column[:name]
|
29
|
+
maxval = column[:maxval].to_i < 1 ? 1 : column[:maxval] + 1
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
@f << <<-EOF
|
34
|
+
-- TRUNCATE #{table.name};
|
35
|
+
TRUNCATE #{PGconn.quote_ident(table.name)} CASCADE;
|
36
|
+
|
37
|
+
EOF
|
38
|
+
if serial_key
|
39
|
+
@f << <<-EOF
|
40
|
+
SELECT pg_catalog.setval(pg_get_serial_sequence('#{table.name}', '#{serial_key}'), #{maxval}, true);
|
41
|
+
EOF
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def write_table(table)
|
46
|
+
primary_keys = []
|
47
|
+
serial_key = nil
|
48
|
+
maxval = nil
|
49
|
+
|
50
|
+
columns = table.columns.map do |column|
|
51
|
+
if column[:auto_increment]
|
52
|
+
serial_key = column[:name]
|
53
|
+
maxval = column[:maxval].to_i < 1 ? 1 : column[:maxval] + 1
|
54
|
+
end
|
55
|
+
if column[:primary_key]
|
56
|
+
primary_keys << column[:name]
|
57
|
+
end
|
58
|
+
" " + column_description(column)
|
59
|
+
end.join(",\n")
|
60
|
+
|
61
|
+
if serial_key
|
62
|
+
|
63
|
+
@f << <<-EOF
|
64
|
+
--
|
65
|
+
-- Name: #{table.name}_#{serial_key}_seq; Type: SEQUENCE; Schema: public
|
66
|
+
--
|
67
|
+
|
68
|
+
DROP SEQUENCE IF EXISTS #{table.name}_#{serial_key}_seq CASCADE;
|
69
|
+
|
70
|
+
CREATE SEQUENCE #{table.name}_#{serial_key}_seq
|
71
|
+
INCREMENT BY 1
|
72
|
+
NO MAXVALUE
|
73
|
+
NO MINVALUE
|
74
|
+
CACHE 1;
|
75
|
+
|
76
|
+
|
77
|
+
SELECT pg_catalog.setval('#{table.name}_#{serial_key}_seq', #{maxval}, true);
|
78
|
+
|
79
|
+
EOF
|
80
|
+
end
|
81
|
+
|
82
|
+
@f << <<-EOF
|
83
|
+
-- Table: #{table.name}
|
84
|
+
|
85
|
+
-- DROP TABLE #{table.name};
|
86
|
+
DROP TABLE IF EXISTS #{PGconn.quote_ident(table.name)} CASCADE;
|
87
|
+
|
88
|
+
CREATE TABLE #{PGconn.quote_ident(table.name)} (
|
89
|
+
EOF
|
90
|
+
|
91
|
+
@f << columns
|
92
|
+
|
93
|
+
if primary_index = table.indexes.find {|index| index[:primary]}
|
94
|
+
@f << ",\n CONSTRAINT #{table.name}_pkey PRIMARY KEY(#{primary_index[:columns].map {|col| PGconn.quote_ident(col)}.join(", ")})"
|
95
|
+
end
|
96
|
+
|
97
|
+
@f << <<-EOF
|
98
|
+
\n)
|
99
|
+
WITHOUT OIDS;
|
100
|
+
EOF
|
101
|
+
|
102
|
+
table.indexes.each do |index|
|
103
|
+
next if index[:primary]
|
104
|
+
unique = index[:unique] ? "UNIQUE " : nil
|
105
|
+
@f << <<-EOF
|
106
|
+
DROP INDEX IF EXISTS #{PGconn.quote_ident(index[:name])} CASCADE;
|
107
|
+
CREATE #{unique}INDEX #{PGconn.quote_ident(index[:name])} ON #{PGconn.quote_ident(table.name)} (#{index[:columns].map {|col| PGconn.quote_ident(col)}.join(", ")});
|
108
|
+
EOF
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
def write_indexes(table)
|
114
|
+
end
|
115
|
+
|
116
|
+
def write_constraints(table)
|
117
|
+
table.foreign_keys.each do |key|
|
118
|
+
@f << "ALTER TABLE #{PGconn.quote_ident(table.name)} ADD FOREIGN KEY (#{PGconn.quote_ident(key[:column])}) REFERENCES #{PGconn.quote_ident(key[:ref_table])}(#{PGconn.quote_ident(key[:ref_column])});\n"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
def write_contents(table, reader)
|
124
|
+
@f << <<-EOF
|
125
|
+
--
|
126
|
+
-- Data for Name: #{table.name}; Type: TABLE DATA; Schema: public
|
127
|
+
--
|
128
|
+
|
129
|
+
COPY "#{table.name}" (#{table.columns.map {|column| PGconn.quote_ident(column[:name])}.join(", ")}) FROM stdin;
|
130
|
+
EOF
|
131
|
+
|
132
|
+
reader.paginated_read(table, 1000) do |row, counter|
|
133
|
+
line = []
|
134
|
+
process_row(table, row)
|
135
|
+
@f << row.join("\t") + "\n"
|
136
|
+
end
|
137
|
+
@f << "\\.\n\n"
|
138
|
+
#@f << "VACUUM FULL ANALYZE #{PGconn.quote_ident(table.name)};\n\n"
|
139
|
+
end
|
140
|
+
|
141
|
+
def close
|
142
|
+
@f.close
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'pg'
|
2
|
+
|
3
|
+
require 'xmysql2psql/writer'
|
4
|
+
|
5
|
+
class Xmysql2psql
|
6
|
+
|
7
|
+
class PostgresWriter < Writer
|
8
|
+
def db_writer?
|
9
|
+
raise StandardError.new("not implemented")
|
10
|
+
end
|
11
|
+
|
12
|
+
def escape_bytea(value)
|
13
|
+
if db_writer?
|
14
|
+
self.conn.escape_bytea(value)
|
15
|
+
else
|
16
|
+
PGConn.escape_bytea(value)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def column_description(column)
|
21
|
+
"#{PGconn.quote_ident(column[:name])} #{column_type_info(column)}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def column_type(column)
|
25
|
+
column_type_info(column).split(" ").first
|
26
|
+
end
|
27
|
+
|
28
|
+
def column_type_info(column)
|
29
|
+
if column[:auto_increment]
|
30
|
+
return "integer DEFAULT nextval('#{column[:table_name]}_#{column[:name]}_seq'::regclass) NOT NULL"
|
31
|
+
end
|
32
|
+
|
33
|
+
default = column[:default] ? " DEFAULT #{column[:default] == nil ? 'NULL' : "'"+PGconn.escape(column[:default])+"'"}" : nil
|
34
|
+
null = column[:null] ? "" : " NOT NULL"
|
35
|
+
type =
|
36
|
+
case column[:type]
|
37
|
+
|
38
|
+
# String types
|
39
|
+
when "char"
|
40
|
+
default = default + "::char" if default
|
41
|
+
"character(#{column[:length]})"
|
42
|
+
when "varchar"
|
43
|
+
default = default + "::character varying" if default
|
44
|
+
"character varying(#{column[:length]})"
|
45
|
+
|
46
|
+
# Integer and numeric types
|
47
|
+
when "integer"
|
48
|
+
default = " DEFAULT #{column[:default].nil? ? 'NULL' : column[:default].to_i}" if default
|
49
|
+
"integer"
|
50
|
+
when "bigint"
|
51
|
+
default = " DEFAULT #{column[:default].nil? ? 'NULL' : column[:default].to_i}" if default
|
52
|
+
"bigint"
|
53
|
+
when "tinyint"
|
54
|
+
default = " DEFAULT #{column[:default].nil? ? 'NULL' : column[:default].to_i}" if default
|
55
|
+
"smallint"
|
56
|
+
|
57
|
+
when "boolean"
|
58
|
+
default = " DEFAULT #{column[:default].to_i == 1 ? 'true' : 'false'}" if default
|
59
|
+
"boolean"
|
60
|
+
when "float"
|
61
|
+
default = " DEFAULT #{column[:default].nil? ? 'NULL' : column[:default].to_f}" if default
|
62
|
+
"real"
|
63
|
+
when "float unsigned"
|
64
|
+
default = " DEFAULT #{column[:default].nil? ? 'NULL' : column[:default].to_f}" if default
|
65
|
+
"real"
|
66
|
+
when "decimal"
|
67
|
+
default = " DEFAULT #{column[:default].nil? ? 'NULL' : column[:default]}" if default
|
68
|
+
"numeric(#{column[:length] || 10}, #{column[:decimals] || 0})"
|
69
|
+
|
70
|
+
when "double precision"
|
71
|
+
default = " DEFAULT #{column[:default].nil? ? 'NULL' : column[:default]}" if default
|
72
|
+
"double precision"
|
73
|
+
|
74
|
+
# Mysql datetime fields
|
75
|
+
when "datetime"
|
76
|
+
default = nil
|
77
|
+
"timestamp without time zone"
|
78
|
+
when "date"
|
79
|
+
default = nil
|
80
|
+
"date"
|
81
|
+
when "timestamp"
|
82
|
+
default = " DEFAULT CURRENT_TIMESTAMP" if column[:default] == "CURRENT_TIMESTAMP"
|
83
|
+
default = " DEFAULT '1970-01-01 00:00'" if column[:default] == "0000-00-00 00:00"
|
84
|
+
default = " DEFAULT '1970-01-01 00:00:00'" if column[:default] == "0000-00-00 00:00:00"
|
85
|
+
"timestamp without time zone"
|
86
|
+
when "time"
|
87
|
+
default = " DEFAULT NOW()" if default
|
88
|
+
"time without time zone"
|
89
|
+
|
90
|
+
when "tinyblob"
|
91
|
+
"bytea"
|
92
|
+
when "mediumblob"
|
93
|
+
"bytea"
|
94
|
+
when "longblob"
|
95
|
+
"bytea"
|
96
|
+
when "blob"
|
97
|
+
"bytea"
|
98
|
+
when "varbinary"
|
99
|
+
"bytea"
|
100
|
+
when "tinytext"
|
101
|
+
"text"
|
102
|
+
when "mediumtext"
|
103
|
+
"text"
|
104
|
+
when "longtext"
|
105
|
+
"text"
|
106
|
+
when "text"
|
107
|
+
"text"
|
108
|
+
when /^enum/
|
109
|
+
default = default + "::character varying" if default
|
110
|
+
enum = column[:type].gsub(/enum|\(|\)/, '')
|
111
|
+
max_enum_size = enum.split(',').map{ |check| check.size() -2}.sort[-1]
|
112
|
+
"character varying(#{max_enum_size}) check( #{column[:name]} in (#{enum}))"
|
113
|
+
else
|
114
|
+
puts "Unknown #{column.inspect}"
|
115
|
+
column[:type].inspect
|
116
|
+
return ""
|
117
|
+
end
|
118
|
+
"#{type}#{default}#{null}"
|
119
|
+
end
|
120
|
+
|
121
|
+
def process_row(table, row)
|
122
|
+
table.columns.each_with_index do |column, index|
|
123
|
+
|
124
|
+
if column[:type] == "time"
|
125
|
+
row[index] = "%02d:%02d:%02d" % [row[index].hour, row[index].minute, row[index].second]
|
126
|
+
end
|
127
|
+
|
128
|
+
if row[index].is_a?(Mysql::Time)
|
129
|
+
row[index] = row[index].to_s.gsub('0000-00-00 00:00', '1970-01-01 00:00')
|
130
|
+
row[index] = row[index].to_s.gsub('0000-00-00 00:00:00', '1970-01-01 00:00:00')
|
131
|
+
end
|
132
|
+
|
133
|
+
if column_type(column) == "boolean"
|
134
|
+
row[index] = row[index] == 1 ? 't' : row[index] == 0 ? 'f' : row[index]
|
135
|
+
end
|
136
|
+
|
137
|
+
if row[index].is_a?(String)
|
138
|
+
if column_type(column) == "bytea"
|
139
|
+
row[index] = escape_bytea(row[index])
|
140
|
+
else
|
141
|
+
row[index] = row[index].gsub(/\\/, '\\\\\\').gsub(/\n/,'\n').gsub(/\t/,'\t').gsub(/\r/,'\r').gsub(/\0/, '')
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
row[index] = '\N' if !row[index]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def truncate(table)
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
data/lib/xmysql2psql.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'xmysql2psql/errors'
|
2
|
+
require 'xmysql2psql/version'
|
3
|
+
require 'xmysql2psql/config'
|
4
|
+
require 'xmysql2psql/converter'
|
5
|
+
require 'xmysql2psql/mysql_reader'
|
6
|
+
require 'xmysql2psql/writer'
|
7
|
+
require 'xmysql2psql/postgres_writer'
|
8
|
+
require 'xmysql2psql/postgres_db_writer.rb'
|
9
|
+
require 'xmysql2psql/postgres_file_writer.rb'
|
10
|
+
|
11
|
+
|
12
|
+
class Xmysql2psql
|
13
|
+
|
14
|
+
attr_reader :options, :reader, :writer
|
15
|
+
|
16
|
+
def initialize(args)
|
17
|
+
help if args.length==1 && args[0] =~ /^-.?|^-*he?l?p?$/i
|
18
|
+
configfile = args[0] || File.expand_path('xmysql2psql.yml')
|
19
|
+
@options = Config.new( configfile, true )
|
20
|
+
end
|
21
|
+
|
22
|
+
def convert
|
23
|
+
@reader = MysqlReader.new( options )
|
24
|
+
|
25
|
+
if options.destfile(nil)
|
26
|
+
@writer = PostgresFileWriter.new(options.destfile)
|
27
|
+
else
|
28
|
+
@writer = PostgresDbWriter.new(options)
|
29
|
+
end
|
30
|
+
|
31
|
+
Converter.new(reader, writer, options).convert
|
32
|
+
end
|
33
|
+
|
34
|
+
def help
|
35
|
+
puts <<EOS
|
36
|
+
MySQL to PostgreSQL Conversion
|
37
|
+
|
38
|
+
EOS
|
39
|
+
exit -2
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
mysql:
|
2
|
+
hostname: localhost
|
3
|
+
port: 3306
|
4
|
+
socket: /tmp/mysql.sock
|
5
|
+
username: somename
|
6
|
+
password: secretpassword
|
7
|
+
database: somename
|
8
|
+
|
9
|
+
destination:
|
10
|
+
# if file is given, output goes to file, else postgres
|
11
|
+
file: somefile
|
12
|
+
postgres:
|
13
|
+
hostname: localhost
|
14
|
+
port: 5432
|
15
|
+
username: somename
|
16
|
+
password: secretpassword
|
17
|
+
database: somename
|
18
|
+
|
19
|
+
# if tables is given, only the listed tables will be converted. leave empty to convert all tables.
|
20
|
+
tables:
|
21
|
+
- table1
|
22
|
+
- table2
|
23
|
+
- table3
|
24
|
+
- table4
|
25
|
+
|
26
|
+
# if exclude_tables is given, exclude the listed tables from the conversion.
|
27
|
+
exclude_tables:
|
28
|
+
- table5
|
29
|
+
- table6
|
30
|
+
|
31
|
+
# if supress_data is true, only the schema definition will be exported/migrated, and not the data
|
32
|
+
supress_data: true
|
33
|
+
|
34
|
+
# if supress_ddl is true, only the data will be exported/imported, and not the schema
|
35
|
+
supress_ddl: false
|
36
|
+
|
37
|
+
# if force_truncate is true, forces a table truncate before table loading
|
38
|
+
force_truncate: false
|
@@ -0,0 +1,24 @@
|
|
1
|
+
-- seed data for integration tests
|
2
|
+
|
3
|
+
DROP TABLE IF EXISTS numeric_types_basics;
|
4
|
+
CREATE TABLE numeric_types_basics (
|
5
|
+
id int,
|
6
|
+
f_tinyint TINYINT,
|
7
|
+
f_smallint SMALLINT,
|
8
|
+
f_mediumint MEDIUMINT,
|
9
|
+
f_int INT,
|
10
|
+
f_integer INTEGER,
|
11
|
+
f_bigint BIGINT,
|
12
|
+
f_real REAL,
|
13
|
+
f_double DOUBLE,
|
14
|
+
f_float FLOAT,
|
15
|
+
f_decimal DECIMAL,
|
16
|
+
f_numeric NUMERIC
|
17
|
+
);
|
18
|
+
|
19
|
+
INSERT INTO numeric_types_basics VALUES
|
20
|
+
(1,1,1,1,1,1,1,1,1,1,1,1),
|
21
|
+
(2,2,2,2,2,2,2,2,2,2,2,2),
|
22
|
+
(23,23,23,23,23,23,23,23,23,23,23,23);
|
23
|
+
|
24
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
require 'xmysql2psql'
|
4
|
+
|
5
|
+
class ConvertToDbTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def startup
|
9
|
+
seed_test_database
|
10
|
+
@@options=get_test_config_by_label(:localmysql_to_db_convert_all)
|
11
|
+
@@xmysql2psql = Xmysql2psql.new([@@options.filepath])
|
12
|
+
@@xmysql2psql.convert
|
13
|
+
@@xmysql2psql.writer.open
|
14
|
+
end
|
15
|
+
def shutdown
|
16
|
+
@@xmysql2psql.writer.close
|
17
|
+
delete_files_for_test_config(@@options)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
def setup
|
21
|
+
end
|
22
|
+
def teardown
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_table_creation
|
26
|
+
assert_true @@xmysql2psql.writer.exists?('numeric_types_basics')
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
require 'xmysql2psql'
|
4
|
+
|
5
|
+
class ConvertToFileTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def startup
|
9
|
+
seed_test_database
|
10
|
+
@@options=get_test_config_by_label(:localmysql_to_file_convert_all)
|
11
|
+
@@xmysql2psql = Xmysql2psql.new([@@options.filepath])
|
12
|
+
@@xmysql2psql.convert
|
13
|
+
@@content = IO.read(@@xmysql2psql.options.destfile)
|
14
|
+
end
|
15
|
+
def shutdown
|
16
|
+
delete_files_for_test_config(@@options)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
def setup
|
20
|
+
end
|
21
|
+
def teardown
|
22
|
+
end
|
23
|
+
def content
|
24
|
+
@@content
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_table_creation
|
28
|
+
assert_not_nil content.match('DROP TABLE IF EXISTS "numeric_types_basics" CASCADE')
|
29
|
+
assert_not_nil content.match(/CREATE TABLE "numeric_types_basics"/)
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_basic_numerics_tinyint
|
33
|
+
assert_not_nil Regexp.new('CREATE TABLE "numeric_types_basics".*"f_tinyint" smallint,.*\)', Regexp::MULTILINE).match( content )
|
34
|
+
end
|
35
|
+
def test_basic_numerics_smallint
|
36
|
+
assert_not_nil Regexp.new('CREATE TABLE "numeric_types_basics".*"f_smallint" integer,.*\)', Regexp::MULTILINE).match( content )
|
37
|
+
end
|
38
|
+
def test_basic_numerics_mediumint
|
39
|
+
assert_not_nil Regexp.new('CREATE TABLE "numeric_types_basics".*"f_mediumint" integer,.*\)', Regexp::MULTILINE).match( content )
|
40
|
+
end
|
41
|
+
def test_basic_numerics_int
|
42
|
+
assert_not_nil Regexp.new('CREATE TABLE "numeric_types_basics".*"f_int" integer,.*\)', Regexp::MULTILINE).match( content )
|
43
|
+
end
|
44
|
+
def test_basic_numerics_integer
|
45
|
+
assert_not_nil Regexp.new('CREATE TABLE "numeric_types_basics".*"f_integer" integer,.*\)', Regexp::MULTILINE).match( content )
|
46
|
+
end
|
47
|
+
def test_basic_numerics_bigint
|
48
|
+
assert_not_nil Regexp.new('CREATE TABLE "numeric_types_basics".*"f_bigint" bigint,.*\)', Regexp::MULTILINE).match( content )
|
49
|
+
end
|
50
|
+
def test_basic_numerics_real
|
51
|
+
assert_not_nil Regexp.new('CREATE TABLE "numeric_types_basics".*"f_real" double precision,.*\)', Regexp::MULTILINE).match( content )
|
52
|
+
end
|
53
|
+
def test_basic_numerics_double
|
54
|
+
assert_not_nil Regexp.new('CREATE TABLE "numeric_types_basics".*"f_double" double precision,.*\)', Regexp::MULTILINE).match( content )
|
55
|
+
end
|
56
|
+
def test_basic_numerics_float
|
57
|
+
assert_not_nil Regexp.new('CREATE TABLE "numeric_types_basics".*"f_float" numeric\(20, 0\),.*\)', Regexp::MULTILINE).match( content )
|
58
|
+
end
|
59
|
+
def test_basic_numerics_decimal
|
60
|
+
assert_not_nil Regexp.new('CREATE TABLE "numeric_types_basics".*"f_decimal" numeric\(10, 0\),.*\)', Regexp::MULTILINE).match( content )
|
61
|
+
end
|
62
|
+
def test_basic_numerics_numeric
|
63
|
+
assert_not_nil Regexp.new('CREATE TABLE "numeric_types_basics".*"f_numeric" numeric\(10, 0\)[\w\n]*\)', Regexp::MULTILINE).match( content )
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|