sqlite2mysql 0.1.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e4abd7634da6d7d4924ac5440ca84a74328e64e0
4
- data.tar.gz: 673c82ad3762793877b8d8b9671439b80e51bd47
3
+ metadata.gz: 4459055881f5d71cbb6f70ba64ac6370f414b6b9
4
+ data.tar.gz: c7b60bb262fa636a7d53efd98a3d54bf9bd13f88
5
5
  SHA512:
6
- metadata.gz: 0a7bf80a0be02f3032275bffbf3510328f9618f6d88b08b294ba9a0277d1c3128ba6bd34ae056386ba487845d2f93cd20dfdb3574eb5c57d3cb37c0dc86971b3
7
- data.tar.gz: b36b4cd35cdd4cb184fa16bb238ee4b221769d51e76d8969f8e95407711a43b0ca9573556002699b4e023a47de35dc0da34df238493c14fbed3a70ab45c37a71
6
+ metadata.gz: 4a0b2dbf1af68fe5c2e52029e54f9c71d1133d25808401a83764fddde1b6d2490781affc9a56592b349b1e0471432c9524e540b49f305d18a601bb8e1e548616
7
+ data.tar.gz: 120080c429c5d492778e2a8d7b587e5f5762c90dcf578c273845c2856155aff7239502c90293567ef0ac66de3740c897d37047e7ad3859f5061794c2f99b6a10
@@ -1,78 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'mysql2'
4
- require 'sqlite3'
3
+ require 'sqlite2mysql'
5
4
 
6
- puts 'Usage: sqlite2mysql sqlite_file.db [mysql_db_name]' if ARGV.size < 1
7
-
8
- DATABASE = ARGV.first
9
- SQL_DB_NAME = ARGV[1] || DATABASE.gsub(/[^0-9a-z]/i, '')
10
-
11
- puts 'Collecting Sqlit3 Info' # ===============================================
12
-
13
- db = SQLite3::Database.new DATABASE
14
-
15
- schema = {}
16
-
17
- tables = db.execute 'SELECT name FROM sqlite_master WHERE type="table"'
18
-
19
- tables.flatten.each do |t|
20
- columns = db.execute("pragma table_info(#{t})")
21
-
22
- formatted_columns = []
23
- columns.each do |col|
24
- formatted_columns << { name: col[1],
25
- type: col[2],
26
- notnull: col[3],
27
- default: col[4] }
28
- end
29
-
30
- schema[t] = formatted_columns
31
- end
32
-
33
- puts "Creating MySQL DB: #{SQL_DB_NAME}" # ====================================
34
-
35
- RESERVED_WORDS = %w(key int)
36
-
37
- def create_table_query(table, columns)
38
- query = "CREATE TABLE #{table} ("
39
- cols = []
40
- columns.each do |col|
41
- col[:name] += '_1' if RESERVED_WORDS.include?(col[:name])
42
- if col[:type] == ''
43
- col[:type] = 'varchar(255)'
44
- elsif col[:type].start_with?('float')
45
- col[:type] = 'float'
46
- end
47
- cols << "#{col[:name]} #{col[:type]} #{'NOT NULL' if col[:notnull]}"
48
- end
49
- query + "#{cols.join(', ')})"
50
- end
51
-
52
- client = Mysql2::Client.new(host: 'localhost', username: 'root')
53
-
54
- client.query("DROP DATABASE IF EXISTS #{SQL_DB_NAME}")
55
- client.query("CREATE DATABASE #{SQL_DB_NAME}")
56
- client.query("USE #{SQL_DB_NAME}")
57
-
58
- schema.keys.each do |table|
59
- puts "Creating table: #{table}"
60
- client.query(create_table_query(table, schema[table]))
61
- end
62
-
63
- print 'Grab a ☕' # ============================================================
64
-
65
- schema.keys.each do |table|
66
- puts "\nInserting data: #{table}"
67
- data = db.execute("select * from #{table}")
68
- data.each_slice(1000) do |slice|
69
- slice.each do |row|
70
- cleaned_row = row.map do |val|
71
- val.is_a?(String) ? client.escape(val) : val
72
- end
73
- client.query("INSERT INTO #{table} VALUES (\"#{cleaned_row.join('", "')}\")")
74
- end
75
- print '.'
76
- end
77
- end
78
- puts ''
5
+ Sqlite2Mysql.run(ARGV)
@@ -1,3 +1,39 @@
1
1
  require 'sqlite2mysql/version'
2
+ require 'sqlite2mysql/services/arguments'
3
+ require 'sqlite2mysql/services/bound_finder'
4
+ require 'sqlite2mysql/services/mysql'
5
+ require 'sqlite2mysql/services/sqlite'
6
+ require 'sqlite2mysql/services/type_inferrer'
2
7
 
3
- puts 'WARNING: Including sqlite2mysql does nothing, run it from the terminal.'
8
+ class Sqlite2Mysql
9
+ class << self
10
+ def run(args)
11
+ arguments = Arguments.new(args)
12
+
13
+ puts 'Collecting Sqlite3 Info'
14
+
15
+ db = SqliteClient.new(arguments.sqlite_db, infer_column_types: arguments.infer_types)
16
+
17
+ schema = db.build_schema
18
+
19
+ puts "Creating MySQL DB: #{arguments.mysql_db}"
20
+
21
+ mysql = MysqlClient.new(
22
+ host: arguments.mysql_host,
23
+ username: arguments.username,
24
+ password: arguments.password,
25
+ port: arguments.mysql_port)
26
+ mysql.recreate(arguments.mysql_db)
27
+ mysql.build_from_schema(schema)
28
+
29
+ print 'Grab a ☕'
30
+
31
+ schema.keys.each do |table|
32
+ puts "\nInserting data: #{table}"
33
+ data = db.get_data(table)
34
+ mysql.insert_table(table, data)
35
+ end
36
+ puts ''
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,71 @@
1
+ class Arguments
2
+ attr_accessor :mysql_host, :username, :password, :mysql_port, :infer_types,
3
+ :mysql_db, :sqlite_db
4
+
5
+ def initialize(args)
6
+ help(args) if args.size == 0
7
+
8
+ set_defaults
9
+ unmodified_args = args.dup
10
+ unmodified_args.each do |arg|
11
+ if arg.start_with?('--')
12
+ send(arg[2..-1], args)
13
+ args.delete(arg)
14
+ end
15
+ end
16
+
17
+ @sqlite_db = args.first
18
+ @mysql_db = args[1] || @sqlite_db.gsub(/[^0-9a-z]/i, '')
19
+ end
20
+
21
+ def infer(_)
22
+ @infer_types = true
23
+ end
24
+
25
+ def user(args)
26
+ @username = get_value_for_flag('--user', args)
27
+ end
28
+
29
+ def pass(args)
30
+ @password = get_value_for_flag('--pass', args)
31
+ end
32
+
33
+ def port(args)
34
+ @mysql_port = get_value_for_flag('--port', args)
35
+ end
36
+
37
+ def host(args)
38
+ @mysql_host = get_value_for_flag('--host', args)
39
+ end
40
+
41
+ def help(_)
42
+ puts <<-HELP
43
+ Usage:
44
+ sqlite2mysql sqlite.db [mysql_name]
45
+
46
+ Options:
47
+ --help Show this message
48
+ --infer Infer types for columns
49
+ --user MySQL username (default: root)
50
+ --host MySQL host (default: localhost)
51
+ --pass MySQL password
52
+ --port MySQL port
53
+ HELP
54
+ exit 0
55
+ end
56
+
57
+ private
58
+
59
+ def get_value_for_flag(flag, args)
60
+ index = args.index(flag)
61
+ args.delete_at(index + 1)
62
+ end
63
+
64
+ def set_defaults
65
+ @username = 'root'
66
+ @password = nil
67
+ @host = 'localhost'
68
+ @port = nil
69
+ @infer = false
70
+ end
71
+ end
@@ -0,0 +1,23 @@
1
+ class BoundFinder
2
+ def initialize(client, table, column)
3
+ @client = client
4
+ @table = table
5
+ @column = column
6
+ end
7
+
8
+ def max
9
+ @client.select("MAX(#{@column})", @table)
10
+ end
11
+
12
+ def min
13
+ @client.select("MIN(#{@column})", @table)
14
+ end
15
+
16
+ def max_length
17
+ @client.select("MAX(LENGTH(#{@column}))", @table)
18
+ end
19
+
20
+ def min_length
21
+ @client.select("MIN(LENGTH(#{@column}))", @table)
22
+ end
23
+ end
@@ -0,0 +1,64 @@
1
+ require 'mysql2'
2
+
3
+ class MysqlClient
4
+ def initialize(*args)
5
+ @client = Mysql2::Client.new(*args)
6
+ end
7
+
8
+ def recreate(name)
9
+ @client.query("DROP DATABASE IF EXISTS #{name}")
10
+ @client.query("CREATE DATABASE #{name}")
11
+ @client.query("USE #{name}")
12
+ end
13
+
14
+ def build_from_schema(schema)
15
+ schema.keys.each do |table|
16
+ puts "Creating table: #{table}"
17
+ create_table(table, schema[table])
18
+ end
19
+ end
20
+
21
+ def create_table(table, fields)
22
+ puts create_table_query(table, fields)
23
+ @client.query(create_table_query(table, fields))
24
+ end
25
+
26
+ def insert_table(table, data)
27
+ data.each_slice(1000) do |slice|
28
+ @client.query(chunk_sql(table, slice))
29
+ print '.'
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def chunk_sql(table, chunk)
36
+ values = []
37
+ chunk.each do |row|
38
+ values << "#{row_sql(row)}"
39
+ end
40
+ "INSERT INTO #{table} VALUES #{values.join(', ')}"
41
+ end
42
+
43
+ def row_sql(row)
44
+ values = row.map do |val|
45
+ if val.is_a?(String)
46
+ (val.empty? || val.nil? || val == '') ? nil : @client.escape(val)
47
+ else
48
+ val
49
+ end
50
+ end
51
+ "(\"#{values.join('", "')}\")"
52
+ end
53
+
54
+ def create_table_query(table, fields)
55
+ reserved_words = %w(key int)
56
+ query = "CREATE TABLE #{table} ("
57
+ cols = []
58
+ fields.each do |col|
59
+ col[:name] += '_1' if reserved_words.include?(col[:name])
60
+ cols << "#{col[:name]} #{col[:type]} #{'NOT NULL' if col[:notnull]}"
61
+ end
62
+ query + "#{cols.join(', ')})"
63
+ end
64
+ end
@@ -0,0 +1,58 @@
1
+ require 'sqlite3'
2
+ require 'time'
3
+
4
+ class SqliteClient
5
+ def initialize(filename, infer_column_types: false)
6
+ @db = SQLite3::Database.new(filename)
7
+ @infer = infer_column_types
8
+ end
9
+
10
+ def build_schema
11
+ schema = {}
12
+ tables = @db.execute 'SELECT name FROM sqlite_master WHERE type="table"'
13
+
14
+ tables.flatten.each do |t|
15
+ schema[t] = column_formatter(t)
16
+ end
17
+
18
+ schema
19
+ end
20
+
21
+ def get_data(table)
22
+ @db.execute("select * from #{table}")
23
+ end
24
+
25
+ def column_formatter(table)
26
+ columns = @db.execute("pragma table_info(#{table})")
27
+
28
+ formatted_columns = []
29
+ columns.each do |col|
30
+ formatted_columns << { name: col[1],
31
+ type: type_getter(col[2], table, col[1]),
32
+ notnull: col[3],
33
+ default: col[4] }
34
+ end
35
+ formatted_columns
36
+ end
37
+
38
+ def type_getter(type, table, column)
39
+ if @infer
40
+ samples = @db.execute("SELECT #{column} FROM #{table} WHERE #{column} IS NOT NULL AND #{column} != '' ORDER BY RANDOM() LIMIT 100").flatten
41
+ type = TypeInferrer.new(samples, BoundFinder.new(self, table, column)).make_inference
42
+ puts "Inferring type of #{column} as #{type}"
43
+ return type
44
+ else
45
+ if type == '' || type.nil?
46
+ return 'varchar(255)'
47
+ elsif type.start_with?('float')
48
+ return 'float'
49
+ else
50
+ return type
51
+ end
52
+ end
53
+ end
54
+
55
+ def select(column, table)
56
+ @db.execute("SELECT #{column} FROM #{table}").flatten.first
57
+ end
58
+ end
@@ -0,0 +1,85 @@
1
+ class TypeInferrer
2
+ def initialize(samples, bound_finder)
3
+ @samples = samples
4
+ @bound_finder = bound_finder
5
+ end
6
+
7
+ def make_inference
8
+ possibilities = weigh_possibilities
9
+
10
+ case possibilities.max_by(&:last).first
11
+ when :int
12
+ return get_integer_type
13
+ when :float
14
+ return 'FLOAT'
15
+ when :date
16
+ return 'DATE'
17
+ when :datetime
18
+ return 'DATETIME'
19
+ when :string
20
+ return get_varchar_type
21
+ end
22
+ end
23
+
24
+ def weigh_possibilities
25
+ {
26
+ float: 0,
27
+ int: 0,
28
+ date: 0,
29
+ datetime: 0,
30
+ string: 0
31
+ }.tap do |possibilities|
32
+ @samples.each do |sample|
33
+ if date_or_time?(sample)
34
+ if sample.is_a?(Date) || Date.parse(sample).to_time == Time.parse(sample)
35
+ possibilities[:date] += 1
36
+ else
37
+ possibilities[:datetime] += 1
38
+ end
39
+ elsif sample.is_a?(Float) || sample.to_i > 0 && sample.to_f != sample.to_i.to_f
40
+ possibilities[:float] += 1
41
+ elsif sample.is_a?(Integer) || sample.to_i > 0 || sample == '0'
42
+ possibilities[:int] += 1
43
+ else
44
+ possibilities[:string] += 1
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def get_integer_type
51
+ max = @bound_finder.max.to_i
52
+ min = @bound_finder.min.to_i
53
+ if min > -128 && max < 127
54
+ 'TINYINT'
55
+ elsif min > -32768 && max < 32767
56
+ 'SMALLINT'
57
+ elsif min > -8388608 && max < 8388607
58
+ 'MEDIUMINT'
59
+ elsif min > -2147483648 && max < 2147483647
60
+ 'INT'
61
+ else
62
+ 'BIGINT'
63
+ end
64
+ end
65
+
66
+ def get_varchar_type
67
+ max_length = @bound_finder.max_length
68
+ max_length = 1 if max_length == 0 || max_length.nil?
69
+
70
+ return 'TEXT' if max_length > 255
71
+
72
+ "VARCHAR(#{max_length})"
73
+ end
74
+
75
+ private
76
+
77
+ def date_or_time?(sample)
78
+ sample.is_a?(Date) ||
79
+ sample.is_a?(Time) ||
80
+ sample.is_a?(String) &&
81
+ (%r((\d{1,2}[-\/]\d{1,2}[-\/]\d{4})|(\d{4}[-\/]\d{1,2}[-\/]\d{1,2})).match(sample) &&
82
+ Date.parse(sample) rescue false &&
83
+ Time.parse(sample) rescue false)
84
+ end
85
+ end
@@ -1,3 +1,3 @@
1
1
  module Sqlite2mysql
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/readme.md CHANGED
@@ -0,0 +1,29 @@
1
+ # sqlite2mysql
2
+
3
+ ### Installation
4
+
5
+ gem install sqlite2mysql
6
+
7
+ Don't include it in your projects, that's not what it's for. This is a command line tool.
8
+
9
+ ### Usage
10
+
11
+ Run like this:
12
+
13
+ sqlite2mysql test.db
14
+
15
+ This will create a database called testdb in mysql with the exact schema and data as was in `test.db`. You can optionally name it something else in mysql like this:
16
+
17
+ sqlite2mysql test.db my_awesome_database
18
+
19
+ Isn't that handy?
20
+
21
+ This assumes you can log in as root to your localhost mysql database without a password.
22
+
23
+ ### Contributing
24
+
25
+ Do.
26
+
27
+ ### License
28
+
29
+ MIT.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sqlite2mysql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Standke
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-09-04 00:00:00.000000000 Z
11
+ date: 2015-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -100,6 +100,11 @@ files:
100
100
  - bin/setup
101
101
  - exe/sqlite2mysql
102
102
  - lib/sqlite2mysql.rb
103
+ - lib/sqlite2mysql/services/arguments.rb
104
+ - lib/sqlite2mysql/services/bound_finder.rb
105
+ - lib/sqlite2mysql/services/mysql.rb
106
+ - lib/sqlite2mysql/services/sqlite.rb
107
+ - lib/sqlite2mysql/services/type_inferrer.rb
103
108
  - lib/sqlite2mysql/version.rb
104
109
  - readme.md
105
110
  - sqlite2mysql.gemspec
@@ -128,3 +133,4 @@ signing_key:
128
133
  specification_version: 4
129
134
  summary: Simple tool to convert sqlite3 to mysql
130
135
  test_files: []
136
+ has_rdoc: