xmysql2psql 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|