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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 68e6a0393134773b9525654b5b9f943634a3616c
|
4
|
+
data.tar.gz: ac5b3c5ec00fbc003a276d0bb41c5e407d67a57e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cffc0de72ddc657ae62f74200a3bd6a96037fec2bcee6d5606dadacdd937c20beaa26987709405f65d90e8c25a0120f5b6f1e870a282a66e6759f49802da2a8a
|
7
|
+
data.tar.gz: f224a0393a3da4496902176d2e8bfff9b84ac9c3bb54f610549c99f9e1a2af447a77b474c6312a877c279232eb08ab8c404f47d6db81be5d00fbc56d511ec0bd
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.gem
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009-2010 name <email>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
= XMYSQL2PSQL - MySQL to PostgreSQL Migration Tool
|
2
|
+
|
3
|
+
Tool for converting mysql database to postgresql.
|
4
|
+
It can create postgresql dump from mysql database or directly load data from mysql to
|
5
|
+
postgresql (at about 100 000 records per minute).
|
6
|
+
|
7
|
+
It can translate now most data types and indexes, but if you experience some problems, feel free
|
8
|
+
to contact me, I'll help you.
|
9
|
+
|
10
|
+
== Installation
|
11
|
+
Xmysql2psql is packaged as a gem. Install as usual (use sudo if required).
|
12
|
+
|
13
|
+
$ gem install mysql2psql
|
14
|
+
NB: the gem hasn't been formally released on http://rubygems.org/ yet. For now you need to clone the git repo at http://github.com/tardate/mysql2postgres and 'rake install'
|
15
|
+
|
16
|
+
After installation, the "mysql2psql" command will be available.
|
17
|
+
When run, it will generate a default configuration file in the current directory if a config
|
18
|
+
file is not already found. The default configuration filename is mysql2psql.yml.
|
19
|
+
|
20
|
+
$ xmysql2psql
|
21
|
+
|
22
|
+
You can use an alternate config file by passing the filename/path as a parameter:
|
23
|
+
|
24
|
+
$ xmysql2psql ../another/world.yml
|
25
|
+
|
26
|
+
== Running Migrations
|
27
|
+
See the configuration file for documentation about settings. Key choices include:
|
28
|
+
* whether to dump to a file, or migrate direct to PostgreSQL
|
29
|
+
* migrate only the schema definition, only the data, or both schema and data
|
30
|
+
|
31
|
+
|
32
|
+
After editing the configuration file and launching tool, you will see something like..
|
33
|
+
|
34
|
+
Creating table friendships...
|
35
|
+
Created table friendships
|
36
|
+
Loading friendships...
|
37
|
+
620000 of 638779 rows loaded. [ETA: 2010/01/21 21:32 (00h:00m:01s)]
|
38
|
+
638779 rows loaded in 1min 3s
|
39
|
+
Indexing table friendships...
|
40
|
+
Indexed table friendships
|
41
|
+
Table creation 0 min, loading 1 min, indexing 0 min, total 1 min
|
42
|
+
|
43
|
+
|
44
|
+
== Database driver dependencies
|
45
|
+
Xmysql2psql uses the 'mysql2' and 'pg' gems for database connectivity.
|
46
|
+
Xmysql2psql gem installation should automatically install these too.
|
47
|
+
|
48
|
+
If you encounter any issues with db connectivity, verify that the 'mysql' and 'pg' gems are installed and working correctly first.
|
49
|
+
The 'mysql' gem in particular may need a hint on where to find the mysql headers and libraries:
|
50
|
+
|
51
|
+
$ [sudo] gem install mysql -- --with-mysql-dir=/usr/local/mysql
|
52
|
+
|
53
|
+
== Getting the source
|
54
|
+
Xmysql2psql's source is located at http://github.com/kmizu/xmysql2psql
|
55
|
+
|
56
|
+
== Running tests
|
57
|
+
If you fork/clone the project, you will want to run the test suite (and add to it if you are developing new features or fixing bugs).
|
58
|
+
|
59
|
+
A suite of tests are provided and are run using rake (rake -T for more info)
|
60
|
+
rake test:units
|
61
|
+
rake test:integration
|
62
|
+
rake test
|
63
|
+
|
64
|
+
Rake without parameters (or "rake test") will run both the unit and integration tests.
|
65
|
+
|
66
|
+
Unit tests are small standalone tests of the xmysql2psql codebase that have no external dependencies (like a database)
|
67
|
+
|
68
|
+
Integration tests require suitable mysql and postgres databases to be setup in advance.
|
69
|
+
Running the integration tests *will* rewrite data and schema for the "xmysql2psql_test" databases in mysql and postgres. Don't use this database for anything else!
|
70
|
+
|
71
|
+
mysql on localhost:3306
|
72
|
+
- database created called "xmysql2psql_test"
|
73
|
+
- user setup for "xmysql2psql" with no password
|
74
|
+
- e.g.
|
75
|
+
mysqladmin -uroot -p create xmysql2psql_test
|
76
|
+
mysql -uroot -p -e "grant all on xmysql2psql_test.* to 'mysql2psql'@'localhost';"
|
77
|
+
# verify connecction:
|
78
|
+
mysql -uxmysql2psql -e "select database(),'yes' as connected" mysql2psql_test
|
79
|
+
|
80
|
+
postgres on localhost:5432
|
81
|
+
- database created called "xmysql2psql_test"
|
82
|
+
- role (user) access setup for "xmysql2psql" with no password
|
83
|
+
- e.g.
|
84
|
+
psql postgres -c "create role xmysql2psql with login"
|
85
|
+
psql postgres -c "create database xmysql2psql_test with owner xmysql2psql encoding='UTF8'"
|
86
|
+
# verify connection:
|
87
|
+
psql xmysql2psql_test -U xmysql2psql -c "\c"
|
88
|
+
|
89
|
+
== Notes, Limitations, Outstanding Issues..
|
90
|
+
|
91
|
+
Todo:
|
92
|
+
- more tests
|
93
|
+
- release gem
|
94
|
+
- a windows cmd shim
|
95
|
+
|
96
|
+
=== note from mgkimsal
|
97
|
+
I'm still having trouble with bit(1)/boolean fields
|
98
|
+
workaround I've found is to put output in file
|
99
|
+
then in VIM on file, search/replace the true/false binary fields with t/f
|
100
|
+
specifically
|
101
|
+
|
102
|
+
(reversed on 3/23 - should be ^A gets f)
|
103
|
+
:%s/^@/t/g
|
104
|
+
:%s/^A/f/g
|
105
|
+
keystrokes are ctrl-v ctrl-shift-@ to get the 'true' binary field
|
106
|
+
keystrokes are ctrl-v ctrl-shift-A to get the 'false' binary field
|
107
|
+
|
108
|
+
== Contributors
|
109
|
+
Project founded by Max Lapshin <max@maxidoors.ru>
|
110
|
+
|
111
|
+
Other contributors (in git log order):
|
112
|
+
- Anton Ageev <anton@ageev.name>
|
113
|
+
- Samuel Tribehou <cracoucax@gmail.com>
|
114
|
+
- Marco Nenciarini <marco.nenciarini@devise.it>
|
115
|
+
- James Nobis <jnobis@jnobis.controldocs.com>
|
116
|
+
- quel <github@quelrod.net>
|
117
|
+
- Holger Amann <keeney@fehu.org>
|
118
|
+
- Maxim Dobriakov <closer.main@gmail.com>
|
119
|
+
- Michael Kimsal <mgkimsal@gmail.com>
|
120
|
+
- Jacob Coby <jcoby@portallabs.com>
|
121
|
+
- Neszt Tibor <neszt@tvnetwork.hu>
|
122
|
+
- Miroslav Kratochvil <exa.exa@gmail.com>
|
123
|
+
- Paul Gallagher <gallagher.paul@gmail.com>
|
data/Rakefile
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift('lib')
|
5
|
+
require 'xmysql2psql/version'
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'jeweler'
|
9
|
+
Jeweler::Tasks.new do |gem|
|
10
|
+
gem.name = "xmysql2psql"
|
11
|
+
gem.version = Xmysql2psql::Version::STRING
|
12
|
+
gem.summary = %Q{Tool for converting mysql database to postgresql}
|
13
|
+
gem.description = %Q{It can create postgresql dump from mysql database or directly load data from mysql to
|
14
|
+
postgresql (at about 100 000 records per minute). Translates most data types and indexes.}
|
15
|
+
gem.email = "mizukota@gmail.com"
|
16
|
+
gem.homepage = "http://github.com/kmizu/xmysql2psql"
|
17
|
+
gem.authors = [
|
18
|
+
"Kota Mizushima <mizukota@gmail.com>"
|
19
|
+
]
|
20
|
+
gem.add_dependency "mysql", "= 2.8.1"
|
21
|
+
gem.add_dependency "pg", ">= 0.15.0"
|
22
|
+
gem.add_development_dependency "test-unit", ">= 2.1.1"
|
23
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
24
|
+
end
|
25
|
+
Jeweler::GemcutterTasks.new
|
26
|
+
rescue LoadError
|
27
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
28
|
+
end
|
29
|
+
|
30
|
+
require 'rake/tasklib'
|
31
|
+
require 'rake/testtask'
|
32
|
+
require 'rdoc/task'
|
33
|
+
namespace :test do
|
34
|
+
Rake::TestTask.new(:units) do |test|
|
35
|
+
test.libs << 'lib' << 'test/lib'
|
36
|
+
test.pattern = 'test/units/*test.rb'
|
37
|
+
test.verbose = true
|
38
|
+
end
|
39
|
+
|
40
|
+
Rake::TestTask.new(:integration) do |test|
|
41
|
+
test.libs << 'lib' << 'test/lib'
|
42
|
+
test.pattern = 'test/integration/*test.rb'
|
43
|
+
test.verbose = true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
desc "Run all tests"
|
48
|
+
task :test do
|
49
|
+
Rake::Task['test:units'].invoke
|
50
|
+
Rake::Task['test:integration'].invoke
|
51
|
+
end
|
52
|
+
|
53
|
+
begin
|
54
|
+
require 'rcov/rcovtask'
|
55
|
+
Rcov::RcovTask.new do |test|
|
56
|
+
test.libs << 'test'
|
57
|
+
test.pattern = 'test/**/*test.rb'
|
58
|
+
test.verbose = true
|
59
|
+
end
|
60
|
+
rescue LoadError
|
61
|
+
task :rcov do
|
62
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
task :default => :test
|
68
|
+
|
69
|
+
require 'rake/tasklib'
|
70
|
+
require 'rake/testtask'
|
71
|
+
require 'rdoc/task'
|
72
|
+
Rake::RDocTask.new do |rdoc|
|
73
|
+
version = Xmysql2psql::Version::STRING
|
74
|
+
|
75
|
+
rdoc.rdoc_dir = 'rdoc'
|
76
|
+
rdoc.title = "xmysql2psql #{version}"
|
77
|
+
rdoc.rdoc_files.include('README*')
|
78
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
79
|
+
end
|
data/bin/xmysql2psql
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'xmysql2psql/config_base'
|
2
|
+
|
3
|
+
class Xmysql2psql
|
4
|
+
|
5
|
+
class Config < ConfigBase
|
6
|
+
|
7
|
+
def initialize(configfilepath, generate_default_if_not_found = true)
|
8
|
+
unless File.exists?(configfilepath)
|
9
|
+
reset_configfile(configfilepath) if generate_default_if_not_found
|
10
|
+
if File.exists?(configfilepath)
|
11
|
+
raise Xmysql2psql::ConfigurationFileInitialized.new("\n
|
12
|
+
No configuration file found.
|
13
|
+
A new file has been initialized at: #{configfilepath}
|
14
|
+
Please review the configuration and retry..\n\n\n")
|
15
|
+
else
|
16
|
+
raise Xmysql2psql::ConfigurationFileNotFound.new("cannot load config file #{configfilepath}")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
super(configfilepath)
|
20
|
+
end
|
21
|
+
|
22
|
+
def reset_configfile(filepath)
|
23
|
+
file = File.new(filepath,'w')
|
24
|
+
self.class.template.each_line do | line|
|
25
|
+
file.puts line
|
26
|
+
end
|
27
|
+
file.close
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.template(to_filename = nil, include_tables = [], exclude_tables = [], supress_data = false, supress_ddl = false, force_truncate = false)
|
31
|
+
configtext = <<EOS
|
32
|
+
mysql:
|
33
|
+
hostname: localhost
|
34
|
+
port: 3306
|
35
|
+
socket: /tmp/mysql.sock
|
36
|
+
username: xmysql2psql
|
37
|
+
password:
|
38
|
+
database: xmysql2psql_test
|
39
|
+
|
40
|
+
destination:
|
41
|
+
# if file is given, output goes to file, else postgres
|
42
|
+
file: #{ to_filename ? to_filename : ''}
|
43
|
+
postgres:
|
44
|
+
hostname: localhost
|
45
|
+
port: 5432
|
46
|
+
username: xmysql2psql
|
47
|
+
password:
|
48
|
+
database: xmysql2psql_test
|
49
|
+
|
50
|
+
# if tables is given, only the listed tables will be converted. leave empty to convert all tables.
|
51
|
+
#tables:
|
52
|
+
#- table1
|
53
|
+
#- table2
|
54
|
+
EOS
|
55
|
+
if include_tables.length>0
|
56
|
+
configtext += "\ntables:\n"
|
57
|
+
include_tables.each do |t|
|
58
|
+
configtext += "- #{t}\n"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
configtext += <<EOS
|
62
|
+
# if exclude_tables is given, exclude the listed tables from the conversion.
|
63
|
+
#exclude_tables:
|
64
|
+
#- table3
|
65
|
+
#- table4
|
66
|
+
|
67
|
+
EOS
|
68
|
+
if exclude_tables.length>0
|
69
|
+
configtext += "\nexclude_tables:\n"
|
70
|
+
exclude_tables.each do |t|
|
71
|
+
configtext += "- #{t}\n"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
if !supress_data.nil?
|
75
|
+
configtext += <<EOS
|
76
|
+
|
77
|
+
# if supress_data is true, only the schema definition will be exported/migrated, and not the data
|
78
|
+
supress_data: #{supress_data}
|
79
|
+
EOS
|
80
|
+
end
|
81
|
+
if !supress_ddl.nil?
|
82
|
+
configtext += <<EOS
|
83
|
+
|
84
|
+
# if supress_ddl is true, only the data will be exported/imported, and not the schema
|
85
|
+
supress_ddl: #{supress_ddl}
|
86
|
+
EOS
|
87
|
+
end
|
88
|
+
if !force_truncate.nil?
|
89
|
+
configtext += <<EOS
|
90
|
+
|
91
|
+
# if force_truncate is true, forces a table truncate before table loading
|
92
|
+
force_truncate: #{force_truncate}
|
93
|
+
EOS
|
94
|
+
end
|
95
|
+
configtext
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'xmysql2psql/errors'
|
3
|
+
|
4
|
+
class Xmysql2psql
|
5
|
+
|
6
|
+
class ConfigBase
|
7
|
+
attr_reader :config, :filepath
|
8
|
+
|
9
|
+
def initialize(configfilepath)
|
10
|
+
@filepath=configfilepath
|
11
|
+
@config = YAML::load(File.read(filepath))
|
12
|
+
end
|
13
|
+
def [](key)
|
14
|
+
self.send( key )
|
15
|
+
end
|
16
|
+
def method_missing(name, *args)
|
17
|
+
token=name.to_s
|
18
|
+
default = args.length>0 ? args[0] : ''
|
19
|
+
must_be_defined = default == :none
|
20
|
+
case token
|
21
|
+
when /mysql/i
|
22
|
+
key=token.sub( /^mysql/, '' )
|
23
|
+
value=config["mysql"][key]
|
24
|
+
when /pg/i
|
25
|
+
key=token.sub( /^pg/, '' )
|
26
|
+
value=config["destination"]["postgres"][key]
|
27
|
+
when /dest/i
|
28
|
+
key=token.sub( /^dest/, '' )
|
29
|
+
value=config["destination"][key]
|
30
|
+
when /only_tables/i
|
31
|
+
value=config["tables"]
|
32
|
+
else
|
33
|
+
value=config[token]
|
34
|
+
end
|
35
|
+
value.nil? ? ( must_be_defined ? (raise Xmysql2psql::UninitializedValueError.new("no value and no default for #{name}")) : default ) : value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
class Xmysql2psql
|
2
|
+
|
3
|
+
class Converter
|
4
|
+
attr_reader :reader, :writer, :options
|
5
|
+
attr_reader :exclude_tables, :only_tables, :supress_data, :supress_ddl, :force_truncate
|
6
|
+
|
7
|
+
def initialize(reader, writer, options)
|
8
|
+
@reader = reader
|
9
|
+
@writer = writer
|
10
|
+
@options = options
|
11
|
+
@exclude_tables = options.exclude_tables([])
|
12
|
+
@only_tables = options.only_tables(nil)
|
13
|
+
@supress_data = options.supress_data(false)
|
14
|
+
@supress_ddl = options.supress_ddl(false)
|
15
|
+
@force_truncate = options.force_truncate(false)
|
16
|
+
end
|
17
|
+
|
18
|
+
def convert
|
19
|
+
_time1 = Time.now
|
20
|
+
|
21
|
+
tables = reader.tables.
|
22
|
+
reject {|table| @exclude_tables.include?(table.name)}.
|
23
|
+
select {|table| @only_tables ? @only_tables.include?(table.name) : true}
|
24
|
+
|
25
|
+
|
26
|
+
tables.each do |table|
|
27
|
+
writer.write_table(table)
|
28
|
+
end unless @supress_ddl
|
29
|
+
|
30
|
+
_time2 = Time.now
|
31
|
+
tables.each do |table|
|
32
|
+
writer.truncate(table) if force_truncate && supress_ddl
|
33
|
+
writer.write_contents(table, reader)
|
34
|
+
end unless @supress_data
|
35
|
+
|
36
|
+
_time3 = Time.now
|
37
|
+
tables.each do |table|
|
38
|
+
writer.write_indexes(table)
|
39
|
+
end unless @supress_ddl
|
40
|
+
tables.each do |table|
|
41
|
+
writer.write_constraints(table)
|
42
|
+
end unless @supress_ddl
|
43
|
+
|
44
|
+
|
45
|
+
writer.close
|
46
|
+
_time4 = Time.now
|
47
|
+
puts "Table creation #{((_time2 - _time1) / 60).round} min, loading #{((_time3 - _time2) / 60).round} min, indexing #{((_time4 - _time3) / 60).round} min, total #{((_time4 - _time1) / 60).round} min"
|
48
|
+
return 0
|
49
|
+
rescue => e
|
50
|
+
$stderr.puts "Xmysql2psql: conversion failed: #{e.to_s}"
|
51
|
+
return -1
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
class Xmysql2psql
|
3
|
+
|
4
|
+
class GeneralError < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
class ConfigurationError < StandardError
|
8
|
+
end
|
9
|
+
class UninitializedValueError < ConfigurationError
|
10
|
+
end
|
11
|
+
class ConfigurationFileNotFound < ConfigurationError
|
12
|
+
end
|
13
|
+
class ConfigurationFileInitialized < ConfigurationError
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'mysql'
|
2
|
+
|
3
|
+
class Xmysql2psql
|
4
|
+
|
5
|
+
class MysqlReader
|
6
|
+
class Field
|
7
|
+
end
|
8
|
+
|
9
|
+
class Table
|
10
|
+
attr_reader :name
|
11
|
+
|
12
|
+
def initialize(reader, name)
|
13
|
+
@reader = reader
|
14
|
+
@name = name
|
15
|
+
end
|
16
|
+
|
17
|
+
@@types = %w(tiny enum decimal short long float double null timestamp longlong int24 date time datetime year set blob string var_string char).inject({}) do |list, type|
|
18
|
+
list[eval("::Mysql::Field::TYPE_#{type.upcase}")] = type
|
19
|
+
list
|
20
|
+
end
|
21
|
+
|
22
|
+
@@types[246] = "decimal"
|
23
|
+
|
24
|
+
def columns
|
25
|
+
@columns ||= load_columns
|
26
|
+
end
|
27
|
+
|
28
|
+
def convert_type(type)
|
29
|
+
case type
|
30
|
+
when /int.* unsigned/
|
31
|
+
"bigint"
|
32
|
+
when /bigint/
|
33
|
+
"bigint"
|
34
|
+
when "bit(1)"
|
35
|
+
"boolean"
|
36
|
+
when "tinyint(1)"
|
37
|
+
"boolean"
|
38
|
+
when /tinyint/
|
39
|
+
"tinyint"
|
40
|
+
when /int/
|
41
|
+
"integer"
|
42
|
+
when /varchar/
|
43
|
+
"varchar"
|
44
|
+
when /char/
|
45
|
+
"char"
|
46
|
+
when /(float|decimal)/
|
47
|
+
"decimal"
|
48
|
+
when /double/
|
49
|
+
"double precision"
|
50
|
+
else
|
51
|
+
type
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def load_columns
|
56
|
+
@reader.reconnect
|
57
|
+
result = @reader.mysql.list_fields(name)
|
58
|
+
mysql_flags = ::Mysql::Field.constants.select {|c| c =~ /FLAG/}
|
59
|
+
fields = []
|
60
|
+
@reader.mysql.query("EXPLAIN `#{name}`") do |res|
|
61
|
+
while field = res.fetch_row do
|
62
|
+
length = field[1][/\((\d+)\)/, 1] if field[1] =~ /\((\d+)\)/
|
63
|
+
length = field[1][/\((\d+),(\d+)\)/, 1] if field[1] =~ /\((\d+),(\d+)\)/
|
64
|
+
desc = {
|
65
|
+
:name => field[0],
|
66
|
+
:table_name => name,
|
67
|
+
:type => convert_type(field[1]),
|
68
|
+
:length => length && length.to_i,
|
69
|
+
:decimals => field[1][/\((\d+),(\d+)\)/, 2],
|
70
|
+
:null => field[2] == "YES",
|
71
|
+
:primary_key => field[3] == "PRI",
|
72
|
+
:auto_increment => field[5] == "auto_increment"
|
73
|
+
}
|
74
|
+
desc[:default] = field[4] unless field[4].nil?
|
75
|
+
fields << desc
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
fields.select {|field| field[:auto_increment]}.each do |field|
|
80
|
+
@reader.mysql.query("SELECT max(`#{field[:name]}`) FROM `#{name}`") do |res|
|
81
|
+
field[:maxval] = res.fetch_row[0].to_i
|
82
|
+
end
|
83
|
+
end
|
84
|
+
fields
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
def indexes
|
89
|
+
load_indexes unless @indexes
|
90
|
+
@indexes
|
91
|
+
end
|
92
|
+
|
93
|
+
def foreign_keys
|
94
|
+
load_indexes unless @foreign_keys
|
95
|
+
@foreign_keys
|
96
|
+
end
|
97
|
+
|
98
|
+
def load_indexes
|
99
|
+
@indexes = []
|
100
|
+
@foreign_keys = []
|
101
|
+
|
102
|
+
@reader.mysql.query("SHOW CREATE TABLE `#{name}`") do |result|
|
103
|
+
explain = result.fetch_row[1]
|
104
|
+
explain.split(/\n/).each do |line|
|
105
|
+
next unless line =~ / KEY /
|
106
|
+
index = {}
|
107
|
+
if match_data = /CONSTRAINT `(\w+)` FOREIGN KEY \(`(\w+)`\) REFERENCES `(\w+)` \(`(\w+)`\)/.match(line)
|
108
|
+
index[:name] = match_data[1]
|
109
|
+
index[:column] = match_data[2]
|
110
|
+
index[:ref_table] = match_data[3]
|
111
|
+
index[:ref_column] = match_data[4]
|
112
|
+
@foreign_keys << index
|
113
|
+
elsif match_data = /KEY `(\w+)` \((.*)\)/.match(line)
|
114
|
+
index[:name] = match_data[1]
|
115
|
+
index[:columns] = match_data[2].split(",").map {|col| col[/`(\w+)`/, 1]}
|
116
|
+
index[:unique] = true if line =~ /UNIQUE/
|
117
|
+
@indexes << index
|
118
|
+
elsif match_data = /PRIMARY KEY .*\((.*)\)/.match(line)
|
119
|
+
index[:primary] = true
|
120
|
+
index[:columns] = match_data[1].split(",").map {|col| col.strip.gsub(/`/, "")}
|
121
|
+
@indexes << index
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def count_rows
|
128
|
+
@reader.mysql.query("SELECT COUNT(*) FROM `#{name}`") do |res|
|
129
|
+
return res.fetch_row[0].to_i
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def has_id?
|
134
|
+
!!columns.find {|col| col[:name] == "id"}
|
135
|
+
end
|
136
|
+
|
137
|
+
def count_for_pager
|
138
|
+
query = has_id? ? 'MAX(id)' : 'COUNT(*)'
|
139
|
+
@reader.mysql.query("SELECT #{query} FROM `#{name}`") do |res|
|
140
|
+
return res.fetch_row[0].to_i
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def query_for_pager
|
145
|
+
query = has_id? ? 'WHERE id >= ? AND id < ?' : 'LIMIT ?,?'
|
146
|
+
"SELECT #{columns.map{|c| "`"+c[:name]+"`"}.join(", ")} FROM `#{name}` #{query}"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def connect
|
151
|
+
@mysql = ::Mysql.connect(@host, @user, @passwd, @db, @port, @sock, @flag)
|
152
|
+
@mysql.query("SET NAMES utf8")
|
153
|
+
end
|
154
|
+
|
155
|
+
def reconnect
|
156
|
+
@mysql.close rescue false
|
157
|
+
connect
|
158
|
+
end
|
159
|
+
|
160
|
+
def initialize(options)
|
161
|
+
@host, @user, @passwd, @db, @port, @sock, @flag =
|
162
|
+
options.mysqlhostname('localhost'), options.mysqlusername,
|
163
|
+
options.mysqlpassword, options.mysqldatabase,
|
164
|
+
options.mysqlport, options.mysqlsocket, Mysql::CLIENT_COMPRESS
|
165
|
+
connect
|
166
|
+
end
|
167
|
+
|
168
|
+
attr_reader :mysql
|
169
|
+
|
170
|
+
def tables
|
171
|
+
@tables ||= @mysql.list_tables.map {|table| Table.new(self, table)}
|
172
|
+
end
|
173
|
+
|
174
|
+
def paginated_read(table, page_size)
|
175
|
+
count = table.count_for_pager
|
176
|
+
return if count < 1
|
177
|
+
statement = @mysql.prepare(table.query_for_pager)
|
178
|
+
counter = 0
|
179
|
+
0.upto((count + page_size)/page_size) do |i|
|
180
|
+
statement.execute(i*page_size, table.has_id? ? (i+1)*page_size : page_size)
|
181
|
+
while row = statement.fetch
|
182
|
+
counter += 1
|
183
|
+
yield(row, counter)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
counter
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|