Skip to content

Commit 136ad18

Browse files
committed
Merge pull request #9 from jcoleman/master
Refactor string escaping & add with/without timezone option
2 parents 8477333 + d3bee02 commit 136ad18

File tree

10 files changed

+239
-74
lines changed

10 files changed

+239
-74
lines changed

Rakefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,11 @@ end
6363
begin
6464
require 'rcov/rcovtask'
6565
Rcov::RcovTask.new do |test|
66-
test.libs << 'test'
66+
test.libs << 'lib'
67+
test.libs << 'test/lib'
6768
test.pattern = 'test/**/*test.rb'
6869
test.verbose = true
70+
test.rcov_opts << "--exclude gems/*"
6971
end
7072
rescue LoadError
7173
task :rcov do

lib/mysql2psql/config.rb

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def reset_configfile(filepath)
2727
file.close
2828
end
2929

30-
def self.template(to_filename = nil, include_tables = [], exclude_tables = [], supress_data = false, supress_ddl = false, force_truncate = false)
30+
def self.template(to_filename = nil, include_tables = [], exclude_tables = [], supress_data = false, supress_ddl = false, supress_sequence_update = false, force_truncate = false, use_timezones = false)
3131
configtext = <<EOS
3232
mysql:
3333
hostname: localhost
@@ -83,6 +83,15 @@ def self.template(to_filename = nil, include_tables = [], exclude_tables = [], s
8383
8484
# if supress_ddl is true, only the data will be exported/imported, and not the schema
8585
supress_ddl: #{supress_ddl}
86+
EOS
87+
end
88+
if !supress_sequence_update.nil?
89+
configtext += <<EOS
90+
91+
# if supress_sequence_update is true, the sequences for serial (auto-incrementing) columns
92+
# will not be update to the current maximum value of that column in the database
93+
# if supress_ddl is not set to true, then this option is implied to be false as well (unless overridden here)
94+
supress_sequence_update: #{supress_sequence_update}
8695
EOS
8796
end
8897
if !force_truncate.nil?
@@ -92,6 +101,15 @@ def self.template(to_filename = nil, include_tables = [], exclude_tables = [], s
92101
force_truncate: #{force_truncate}
93102
EOS
94103
end
104+
if !use_timezones.nil?
105+
configtext += <<EOS
106+
107+
# if use_timezones is true, timestamp/time columns will be created in postgres as "with time zone"
108+
# rather than "without time zone"
109+
use_timezones: false
110+
EOS
111+
end
112+
95113
configtext
96114
end
97115

lib/mysql2psql/converter.rb

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ class Mysql2psql
22

33
class Converter
44
attr_reader :reader, :writer, :options
5-
attr_reader :exclude_tables, :only_tables, :supress_data, :supress_ddl, :force_truncate
5+
attr_reader :exclude_tables, :only_tables, :supress_data, :supress_ddl, :supress_sequence_update, :force_truncate
66

77
def initialize(reader, writer, options)
88
@reader = reader
@@ -12,7 +12,9 @@ def initialize(reader, writer, options)
1212
@only_tables = options.only_tables(nil)
1313
@supress_data = options.supress_data(false)
1414
@supress_ddl = options.supress_ddl(false)
15+
@supress_sequence_update
1516
@force_truncate = options.force_truncate(false)
17+
@use_timezones = options.use_timezones(false)
1618
end
1719

1820
def convert
@@ -22,14 +24,17 @@ def convert
2224
reject {|table| @exclude_tables.include?(table.name)}.
2325
select {|table| @only_tables ? @only_tables.include?(table.name) : true}
2426

27+
tables.each do |table|
28+
writer.write_sequence_update(table, options)
29+
end if !(@supress_sequence_update && @supress_ddl)
2530

2631
tables.each do |table|
27-
writer.write_table(table)
32+
writer.write_table(table, {:use_timezones => @use_timezones})
2833
end unless @supress_ddl
2934

3035
_time2 = Time.now
3136
tables.each do |table|
32-
writer.truncate(table) if force_truncate && supress_ddl
37+
writer.truncate(table) if force_truncate && !supress_ddl
3338
writer.write_contents(table, reader)
3439
end unless @supress_data
3540

lib/mysql2psql/postgres_db_writer.rb

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,47 +27,55 @@ def open
2727
def close
2828
@conn.close
2929
end
30-
30+
3131
def exists?(relname)
3232
rc = @conn.exec("SELECT COUNT(*) FROM pg_class WHERE relname = '#{relname}'")
3333
(!rc.nil?) && (rc.to_a.length==1) && (rc.first.count.to_i==1)
3434
end
3535

36-
def write_table(table)
36+
def write_sequence_update(table, options)
37+
serial_key_column = table.columns.detect do |column|
38+
column[:auto_increment]
39+
end
40+
41+
if serial_key_column
42+
serial_key = serial_key_column[:name]
43+
max_value = serial_key_column[:maxval].to_i < 1 ? 1 : serial_key_column[:maxval] + 1
44+
serial_key_seq = "#{table.name}_#{serial_key}_seq"
45+
46+
if !options.supress_ddl
47+
if @conn.server_version < 80200
48+
@conn.exec("DROP SEQUENCE #{serial_key_seq} CASCADE") if exists?(serial_key_seq)
49+
else
50+
@conn.exec("DROP SEQUENCE IF EXISTS #{serial_key_seq} CASCADE")
51+
end
52+
@conn.exec <<-EOF
53+
CREATE SEQUENCE #{serial_key_seq}
54+
INCREMENT BY 1
55+
NO MAXVALUE
56+
NO MINVALUE
57+
CACHE 1
58+
EOF
59+
end
60+
61+
if !options.supress_sequence_update
62+
puts "Updated sequence #{serial_key_seq} to current value of #{max_value}"
63+
@conn.exec sqlfor_set_serial_sequence(table, serial_key_seq, max_value)
64+
end
65+
end
66+
end
67+
68+
def write_table(table, options)
3769
puts "Creating table #{table.name}..."
3870
primary_keys = []
39-
serial_key = nil
40-
maxval = nil
4171

4272
columns = table.columns.map do |column|
43-
if column[:auto_increment]
44-
serial_key = column[:name]
45-
maxval = column[:maxval].to_i < 1 ? 1 : column[:maxval] + 1
46-
end
4773
if column[:primary_key]
4874
primary_keys << column[:name]
4975
end
50-
" " + column_description(column)
76+
" " + column_description(column, options)
5177
end.join(",\n")
5278

53-
if serial_key
54-
if @conn.server_version < 80200
55-
serial_key_seq = "#{table.name}_#{serial_key}_seq"
56-
@conn.exec("DROP SEQUENCE #{serial_key_seq} CASCADE") if exists?(serial_key_seq)
57-
else
58-
@conn.exec("DROP SEQUENCE IF EXISTS #{table.name}_#{serial_key}_seq CASCADE")
59-
end
60-
@conn.exec <<-EOF
61-
CREATE SEQUENCE #{table.name}_#{serial_key}_seq
62-
INCREMENT BY 1
63-
NO MAXVALUE
64-
NO MINVALUE
65-
CACHE 1
66-
EOF
67-
68-
@conn.exec sqlfor_set_serial_sequence(table,serial_key,maxval)
69-
end
70-
7179
if @conn.server_version < 80200
7280
@conn.exec "DROP TABLE #{PGconn.quote_ident(table.name)} CASCADE;" if exists?(table.name)
7381
else

lib/mysql2psql/postgres_file_writer.rb

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -38,41 +38,53 @@ def truncate(table)
3838
end
3939
end
4040

41-
def write_table(table)
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")
41+
def write_sequence_update(table, options)
42+
serial_key_column = table.columns.detect do |column|
43+
column[:auto_increment]
44+
end
5645

57-
if serial_key
46+
if serial_key_column
47+
serial_key = serial_key_column[:name]
48+
serial_key_seq = "#{table.name}_#{serial_key}_seq"
49+
max_value = serial_key_column[:maxval].to_i < 1 ? 1 : serial_key_column[:maxval] + 1
5850

5951
@f << <<-EOF
6052
--
61-
-- Name: #{table.name}_#{serial_key}_seq; Type: SEQUENCE; Schema: public
53+
-- Name: #{serial_key_seq}; Type: SEQUENCE; Schema: public
6254
--
55+
EOF
56+
57+
if !options.supress_ddl
58+
@f << <<-EOF
59+
DROP SEQUENCE IF EXISTS #{serial_key_seq} CASCADE;
6360
64-
DROP SEQUENCE IF EXISTS #{table.name}_#{serial_key}_seq CASCADE;
65-
66-
CREATE SEQUENCE #{table.name}_#{serial_key}_seq
61+
CREATE SEQUENCE #{serial_key_seq}
6762
INCREMENT BY 1
6863
NO MAXVALUE
6964
NO MINVALUE
7065
CACHE 1;
71-
72-
#{sqlfor_set_serial_sequence(table,serial_key,maxval)}
73-
74-
EOF
66+
EOF
67+
end
68+
69+
if !options.supress_sequence_update
70+
@f << <<-EOF
71+
#{sqlfor_set_serial_sequence(table, serial_key_seq, max_value)}
72+
EOF
73+
end
7574
end
75+
end
76+
77+
def write_table(table, options)
78+
primary_keys = []
79+
serial_key = nil
80+
maxval = nil
81+
82+
columns = table.columns.map do |column|
83+
if column[:primary_key]
84+
primary_keys << column[:name]
85+
end
86+
" " + column_description(column, options)
87+
end.join(",\n")
7688

7789
@f << <<-EOF
7890
-- Table: #{table.name}

lib/mysql2psql/postgres_writer.rb

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,11 @@
55
class Mysql2psql
66

77
class PostgresWriter < Writer
8-
def column_description(column)
9-
"#{PGconn.quote_ident(column[:name])} #{column_type_info(column)}"
8+
def column_description(column, options)
9+
"#{PGconn.quote_ident(column[:name])} #{column_type_info(column, options)}"
1010
end
1111

12-
def column_type(column)
13-
column_type_info(column).split(" ").first
14-
end
15-
16-
def column_type(column)
12+
def column_type(column, options={})
1713
if column[:auto_increment]
1814
'integer'
1915
else
@@ -29,9 +25,9 @@ def column_type(column)
2925
when 'decimal'
3026
"numeric(#{column[:length] || 10}, #{column[:decimals] || 0})"
3127
when 'datetime', 'timestamp'
32-
'timestamp without time zone'
28+
"timestamp with#{options[:use_timezones] ? '' : 'out'} time zone"
3329
when 'time'
34-
'time without time zone'
30+
"time with#{options[:use_timezones] ? '' : 'out'} time zone"
3531
when 'tinyblob', 'mediumblob', 'longblob', 'blob', 'varbinary'
3632
'bytea'
3733
when 'tinytext', 'mediumtext', 'longtext', 'text'
@@ -97,8 +93,8 @@ def column_default(column)
9793
end
9894
end
9995

100-
def column_type_info(column)
101-
type = column_type(column)
96+
def column_type_info(column, options)
97+
type = column_type(column, options)
10298
if type
10399
not_null = !column[:null] || column[:auto_increment] ? ' NOT NULL' : ''
104100
default = column[:default] || column[:auto_increment] ? " DEFAULT #{column_default(column)}" : ''
@@ -131,7 +127,17 @@ def process_row(table, row)
131127
if column_type(column) == "bytea"
132128
row[index] = PGconn.escape_bytea(row[index])
133129
else
134-
row[index] = row[index].gsub(/\\/, '\\\\\\').gsub(/\n/,'\n').gsub(/\t/,'\t').gsub(/\r/,'\r')
130+
if row[index] == '\N' || row[index] == '\.'
131+
row[index] = '\\' + row[index] # Escape our two PostgreSQL-text-mode-special strings.
132+
else
133+
# Awesome side-effect producing conditional. Don't do this at home.
134+
unless row[index].gsub!(/\0/, '').nil?
135+
puts "Removed null bytes from string since PostgreSQL TEXT types don't allow the storage of null bytes."
136+
end
137+
138+
row[index] = row[index].dump
139+
row[index] = row[index].slice(1, row[index].size-2)
140+
end
135141
end
136142
elsif row[index].nil?
137143
# Note: '\N' not "\N" is correct here:
@@ -145,11 +151,11 @@ def process_row(table, row)
145151
def truncate(table)
146152
end
147153

148-
def sqlfor_set_serial_sequence(table,serial_key,maxval)
149-
"SELECT pg_catalog.setval('#{table.name}_#{serial_key}_seq', #{maxval}, true);"
154+
def sqlfor_set_serial_sequence(table, serial_key_seq, max_value)
155+
"SELECT pg_catalog.setval('#{serial_key_seq}', #{max_value}, true);"
150156
end
151-
def sqlfor_reset_serial_sequence(table,serial_key,maxval)
152-
"SELECT pg_catalog.setval(pg_get_serial_sequence('#{table.name}', '#{serial_key}'), #{maxval}, true);"
157+
def sqlfor_reset_serial_sequence(table, serial_key, max_value)
158+
"SELECT pg_catalog.setval(pg_get_serial_sequence('#{table.name}', '#{serial_key}'), #{max_value}, true);"
153159
end
154160

155161
end

test/fixtures/seed_integration_tests.sql

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ INSERT INTO numeric_types_basics VALUES
3131
( 5, 127, 255, 32767, 65535, 8388607, 16777215, 2147483647, 4294967295, 2147483647, 4294967295, 9223372036854775807, 18446744073709551615, 1, 1, 1, 1, 1, 1);
3232

3333

34+
3435
DROP TABLE IF EXISTS basic_autoincrement;
3536
CREATE TABLE basic_autoincrement (
3637
auto_id INT(11) NOT NULL AUTO_INCREMENT,
@@ -85,3 +86,34 @@ INSERT INTO test_boolean_conversion (test_name, tinyint_1) VALUES ('test-true-no
8586
CREATE OR REPLACE VIEW test_view AS
8687
SELECT b.test_name
8788
FROM test_boolean_conversion b;
89+
90+
DROP TABLE IF EXISTS test_null_conversion;
91+
CREATE TABLE test_null_conversion (column_a VARCHAR(10));
92+
INSERT INTO test_null_conversion (column_a) VALUES (NULL);
93+
94+
DROP TABLE IF EXISTS test_datetime_conversion;
95+
CREATE TABLE test_datetime_conversion (
96+
column_a DATETIME,
97+
column_b TIMESTAMP,
98+
column_c DATETIME DEFAULT '0000-00-00',
99+
column_d DATETIME DEFAULT '0000-00-00 00:00',
100+
column_e DATETIME DEFAULT '0000-00-00 00:00:00',
101+
column_f TIME
102+
);
103+
INSERT INTO test_datetime_conversion (column_a, column_f) VALUES ('0000-00-00 00:00', '08:15:30');
104+
105+
DROP TABLE IF EXISTS test_index_conversion;
106+
CREATE TABLE test_index_conversion (column_a VARCHAR(10));
107+
CREATE UNIQUE INDEX test_index_conversion ON test_index_conversion (column_a);
108+
109+
DROP TABLE IF EXISTS test_foreign_keys_child;
110+
DROP TABLE IF EXISTS test_foreign_keys_parent;
111+
CREATE TABLE test_foreign_keys_parent (id INT NOT NULL, PRIMARY KEY (id)) ENGINE=INNODB;
112+
CREATE TABLE test_foreign_keys_child (id INT, test_foreign_keys_parent_id INT,
113+
INDEX par_ind (test_foreign_keys_parent_id),
114+
FOREIGN KEY (test_foreign_keys_parent_id) REFERENCES test_foreign_keys_parent(id) ON DELETE CASCADE
115+
) ENGINE=INNODB;
116+
117+
DROP TABLE IF EXISTS test_enum;
118+
CREATE TABLE test_enum (name ENUM('small', 'medium', 'large'));
119+
INSERT INTO test_enum (name) VALUES ('medium');

0 commit comments

Comments
 (0)