bricolage-mysql 5.26.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +25 -0
- data/RELEASE.md +5 -0
- data/jobclass/my-export.rb +40 -0
- data/jobclass/my-import-delta.rb +66 -0
- data/jobclass/my-import.rb +84 -0
- data/jobclass/my-migrate.rb +116 -0
- data/lib/bricolage/mysqldatasource.rb +363 -0
- data/lib/bricolage-mysql.rb +5 -0
- data/libexec/mys3dump.jar +0 -0
- data/libexec/sqldump +9 -0
- data/libexec/sqldump.Darwin +0 -0
- data/libexec/sqldump.Linux +0 -0
- metadata +83 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 40b5df765b4ebd354186a18ab772d9ea55029f48
|
4
|
+
data.tar.gz: 04beadc0f12491319d8ecbd7bf6cd59f6d832923
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 94fa61e79d6690a6d68349934948cdb1b40721c24855c759464889525d7a6c9947ce457f0618294f6bfb94ace7f132c354220a745e6eff460e4dc878b9146eb9
|
7
|
+
data.tar.gz: a0c9b275eaf45b0f7bb9dc8083b05bbd939d870ab7f7c6d24ce1dc94cc789515e06914776e80f276db5458be9d7b2a226bc9bc8d9cd287eb9ca220f6b976af70
|
data/README.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# bricolage-mysql
|
2
|
+
|
3
|
+
MySQL-related job classes for Bricolage batch job framework.
|
4
|
+
|
5
|
+
## Home Page
|
6
|
+
|
7
|
+
https://github.com/bricolages/bricolage-mysql
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
Add following line in your Gemfile:
|
12
|
+
```
|
13
|
+
gem 'bricolage-mysql'
|
14
|
+
```
|
15
|
+
|
16
|
+
## License
|
17
|
+
|
18
|
+
MIT license.
|
19
|
+
See LICENSES file for details.
|
20
|
+
|
21
|
+
## Credit
|
22
|
+
|
23
|
+
Author: Minero Aoki
|
24
|
+
|
25
|
+
This software is written in working time in Cookpad, Inc.
|
data/RELEASE.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
JobClass.define('my-export') {
|
2
|
+
parameters {|params|
|
3
|
+
params.add SQLFileParam.new(optional: true)
|
4
|
+
params.add DestFileParam.new
|
5
|
+
params.add SrcTableParam.new
|
6
|
+
params.add EnumParam.new('format', %w(json tsv csv), 'Target file format.', default: 'json')
|
7
|
+
params.add OptionalBoolParam.new('gzip', 'If true, compresses target file by gzip.')
|
8
|
+
params.add OptionalBoolParam.new('override', 'If true, clears target file. Otherwise causes error.')
|
9
|
+
params.add OptionalBoolParam.new('sqldump', 'If true, clears use sqldump command to dump, only wheen usable.')
|
10
|
+
params.add DataSourceParam.new('mysql')
|
11
|
+
}
|
12
|
+
|
13
|
+
declarations {|params|
|
14
|
+
sql_statement(params).declarations
|
15
|
+
}
|
16
|
+
|
17
|
+
script {|params, script|
|
18
|
+
script.task(params['data-source']) {|task|
|
19
|
+
task.export sql_statement(params),
|
20
|
+
path: params['dest-file'],
|
21
|
+
format: params['format'],
|
22
|
+
override: params['override'],
|
23
|
+
gzip: params['gzip'],
|
24
|
+
sqldump: params['sqldump']
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
def sql_statement(params)
|
29
|
+
if sql = params['sql-file']
|
30
|
+
sql
|
31
|
+
else
|
32
|
+
srcs = params['src-tables']
|
33
|
+
raise ParameterError, "src-tables must be singleton when no sql-file is given" unless srcs.size == 1
|
34
|
+
src_table_var = srcs.keys.first
|
35
|
+
stmt = SQLStatement.for_string("select * from $#{src_table_var};")
|
36
|
+
stmt.declarations = Declarations.new({src_table_var => src_table_var})
|
37
|
+
stmt
|
38
|
+
end
|
39
|
+
end
|
40
|
+
}
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'bricolage/psqldatasource'
|
2
|
+
require 'bricolage/mysqldatasource'
|
3
|
+
|
4
|
+
JobClass.define('my-import-delta') {
|
5
|
+
parameters {|params|
|
6
|
+
# S3Export
|
7
|
+
params.add SrcTableParam.new(optional: false)
|
8
|
+
params.add DataSourceParam.new('mysql', 'src-ds', 'Source data source.')
|
9
|
+
params.add SQLFileParam.new(optional: true)
|
10
|
+
params.add DataSourceParam.new('s3', 's3-ds', 'Temporary file storage.')
|
11
|
+
params.add DestFileParam.new('s3-prefix', 'PREFIX', 'Temporary S3 prefix.')
|
12
|
+
params.add KeyValuePairsParam.new('dump-options', 'KEY:VALUE', 'dump options.', optional: true)
|
13
|
+
|
14
|
+
# Delete, Load
|
15
|
+
params.add DataSourceParam.new('sql', 'dest-ds', 'Destination data source.')
|
16
|
+
params.add StringParam.new('delete-cond', 'SQL_EXPR', 'DELETE condition.')
|
17
|
+
params.add DestTableParam.new(optional: false)
|
18
|
+
params.add KeyValuePairsParam.new('options', 'OPTIONS', 'Loader options.',
|
19
|
+
optional: true, default: PSQLLoadOptions.new,
|
20
|
+
value_handler: lambda {|value, ctx, vars| PSQLLoadOptions.parse(value) })
|
21
|
+
|
22
|
+
# Misc
|
23
|
+
params.add OptionalBoolParam.new('analyze', 'ANALYZE table after SQL is executed.', default: true)
|
24
|
+
params.add OptionalBoolParam.new('vacuum', 'VACUUM table after SQL is executed.')
|
25
|
+
params.add OptionalBoolParam.new('vacuum-sort', 'VACUUM SORT table after SQL is executed.')
|
26
|
+
|
27
|
+
# All
|
28
|
+
params.add OptionalBoolParam.new('export', 'Runs EXPORT task.')
|
29
|
+
params.add OptionalBoolParam.new('load', 'Runs LOAD task.')
|
30
|
+
params.add OptionalBoolParam.new('gzip', 'Compress Temporary files.')
|
31
|
+
}
|
32
|
+
|
33
|
+
script {|params, script|
|
34
|
+
run_all = !params['export'] && !params['load']
|
35
|
+
|
36
|
+
# S3Export
|
37
|
+
if params['export'] || run_all
|
38
|
+
script.task(params['src-ds']) {|task|
|
39
|
+
task.s3export params['src-tables'].values.first.to_s,
|
40
|
+
params['sql-file'],
|
41
|
+
params['s3-ds'],
|
42
|
+
params['s3-prefix'],
|
43
|
+
params['gzip'],
|
44
|
+
dump_options: params['dump-options']
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
# Load
|
49
|
+
if params['load'] || run_all
|
50
|
+
script.task(params['dest-ds']) {|task|
|
51
|
+
task.transaction {
|
52
|
+
# DELETE
|
53
|
+
task.exec SQLStatement.delete_where(params['delete-cond']) if params['delete-cond']
|
54
|
+
|
55
|
+
# COPY
|
56
|
+
task.load params['s3-ds'], params['s3-prefix'], params['dest-table'],
|
57
|
+
'json', nil, params['options'].merge('gzip' => params['gzip'])
|
58
|
+
}
|
59
|
+
|
60
|
+
# VACUUM, ANALYZE
|
61
|
+
task.vacuum_if params['vacuum'], params['vacuum-sort'], params['dest-table']
|
62
|
+
task.analyze_if params['analyze'], params['dest-table']
|
63
|
+
}
|
64
|
+
end
|
65
|
+
}
|
66
|
+
}
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'bricolage/psqldatasource'
|
2
|
+
require 'bricolage/mysqldatasource'
|
3
|
+
|
4
|
+
JobClass.define('my-import') {
|
5
|
+
parameters {|params|
|
6
|
+
# S3Export
|
7
|
+
params.add SrcTableParam.new(optional: false)
|
8
|
+
params.add DataSourceParam.new('mysql', 'src-ds', 'Source data source.')
|
9
|
+
params.add SQLFileParam.new(optional: true)
|
10
|
+
params.add DataSourceParam.new('s3', 's3-ds', 'Temporary file storage.')
|
11
|
+
params.add DestFileParam.new('s3-prefix', 'PREFIX', 'Temporary S3 prefix.')
|
12
|
+
params.add KeyValuePairsParam.new('dump-options', 'KEY:VALUE', 'dump options.', optional: true)
|
13
|
+
|
14
|
+
# Load
|
15
|
+
params.add DestTableParam.new(optional: false)
|
16
|
+
params.add DataSourceParam.new('sql', 'dest-ds', 'Destination data source.')
|
17
|
+
params.add KeyValuePairsParam.new('options', 'OPTIONS', 'Loader options.',
|
18
|
+
optional: true, default: PSQLLoadOptions.new,
|
19
|
+
value_handler: lambda {|value, ctx, vars| PSQLLoadOptions.parse(value) })
|
20
|
+
params.add SQLFileParam.new('table-def', 'PATH', 'Create table file.')
|
21
|
+
params.add OptionalBoolParam.new('no-backup', 'Do not backup current table with suffix "_old".', default: false)
|
22
|
+
|
23
|
+
# Misc
|
24
|
+
params.add OptionalBoolParam.new('analyze', 'ANALYZE table after SQL is executed.', default: true)
|
25
|
+
params.add OptionalBoolParam.new('vacuum', 'VACUUM table after SQL is executed.')
|
26
|
+
params.add OptionalBoolParam.new('vacuum-sort', 'VACUUM SORT table after SQL is executed.')
|
27
|
+
params.add KeyValuePairsParam.new('grant', 'KEY:VALUE', 'GRANT table after SQL is executed. (required keys: privilege, to)')
|
28
|
+
|
29
|
+
# All
|
30
|
+
params.add OptionalBoolParam.new('export', 'Runs EXPORT task.')
|
31
|
+
params.add OptionalBoolParam.new('put', 'Runs PUT task.')
|
32
|
+
params.add OptionalBoolParam.new('load', 'Runs LOAD task.')
|
33
|
+
params.add OptionalBoolParam.new('gzip', 'Compress Temporary files.')
|
34
|
+
}
|
35
|
+
|
36
|
+
script {|params, script|
|
37
|
+
run_all = !params['export'] && !params['put'] && !params['load']
|
38
|
+
|
39
|
+
# S3Export
|
40
|
+
if params['export'] || run_all
|
41
|
+
script.task(params['src-ds']) {|task|
|
42
|
+
task.s3export params['src-tables'].keys.first,
|
43
|
+
params['sql-file'],
|
44
|
+
params['s3-ds'],
|
45
|
+
params['s3-prefix'],
|
46
|
+
params['gzip'],
|
47
|
+
dump_options: params['dump-options']
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
# Load
|
52
|
+
if params['load'] || run_all
|
53
|
+
script.task(params['dest-ds']) {|task|
|
54
|
+
prev_table = '${dest_table}_old'
|
55
|
+
work_table = '${dest_table}_wk'
|
56
|
+
|
57
|
+
task.transaction {
|
58
|
+
# CREATE
|
59
|
+
task.drop_force prev_table
|
60
|
+
task.drop_force work_table
|
61
|
+
task.exec params['table-def'].replace(/\$\{?dest_table\}?\b/, work_table)
|
62
|
+
|
63
|
+
# COPY
|
64
|
+
task.load params['s3-ds'], params['s3-prefix'], work_table,
|
65
|
+
'json', nil, params['options'].merge('gzip' => params['gzip'])
|
66
|
+
|
67
|
+
# GRANT, ANALYZE
|
68
|
+
task.grant_if params['grant'], work_table
|
69
|
+
task.analyze_if params['analyze'], work_table
|
70
|
+
|
71
|
+
# RENAME
|
72
|
+
task.create_dummy_table '${dest_table}'
|
73
|
+
task.rename_table params['dest-table'].to_s, "#{params['dest-table'].name}_old"
|
74
|
+
task.rename_table work_table, params['dest-table'].name
|
75
|
+
}
|
76
|
+
|
77
|
+
task.drop_force prev_table if params['no-backup']
|
78
|
+
|
79
|
+
# VACUUM: vacuum is needless for newly created table, applying vacuum after exposure is not a problem.
|
80
|
+
task.vacuum_if params['vacuum'], params['vacuum-sort'], params['dest-table'].to_s
|
81
|
+
}
|
82
|
+
end
|
83
|
+
}
|
84
|
+
}
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'bricolage/psqldatasource'
|
2
|
+
|
3
|
+
JobClass.define('my-migrate') {
|
4
|
+
parameters {|params|
|
5
|
+
# Export
|
6
|
+
params.add SrcTableParam.new(optional: false)
|
7
|
+
params.add DataSourceParam.new('mysql', 'src-ds', 'Source data source.')
|
8
|
+
params.add DestFileParam.new('tmp-file', 'PATH', 'Temporary local file path.')
|
9
|
+
params.add OptionalBoolParam.new('sqldump', 'If true, use sqldump command to dump, only on available.', default: true)
|
10
|
+
params.add SQLFileParam.new(optional: true)
|
11
|
+
|
12
|
+
# Put
|
13
|
+
params.add DestFileParam.new('s3-file', 'PATH', 'Temporary S3 file path.')
|
14
|
+
params.add DataSourceParam.new('s3', 's3-ds', 'Temporary file storage.')
|
15
|
+
params.add OptionalBoolParam.new('override', 'If true, overwrite s3 target file. Otherwise causes error.')
|
16
|
+
params.add OptionalBoolParam.new('remove-tmp', 'Removes temporary local files after S3-PUT is succeeded.')
|
17
|
+
|
18
|
+
# Load
|
19
|
+
params.add DestTableParam.new(optional: false)
|
20
|
+
params.add DataSourceParam.new('sql', 'dest-ds', 'Destination data source.')
|
21
|
+
params.add KeyValuePairsParam.new('options', 'OPTIONS', 'Loader options.',
|
22
|
+
optional: true, default: PSQLLoadOptions.new,
|
23
|
+
value_handler: lambda {|value, ctx, vars| PSQLLoadOptions.parse(value) })
|
24
|
+
params.add SQLFileParam.new('table-def', 'PATH', 'Create table file.')
|
25
|
+
params.add OptionalBoolParam.new('no-backup', 'Do not backup current table with suffix "_old".', default: false)
|
26
|
+
|
27
|
+
# Misc
|
28
|
+
params.add OptionalBoolParam.new('analyze', 'ANALYZE table after SQL is executed.', default: true)
|
29
|
+
params.add OptionalBoolParam.new('vacuum', 'VACUUM table after SQL is executed.')
|
30
|
+
params.add OptionalBoolParam.new('vacuum-sort', 'VACUUM SORT table after SQL is executed.')
|
31
|
+
params.add KeyValuePairsParam.new('grant', 'KEY:VALUE', 'GRANT table after SQL is executed. (required keys: privilege, to)')
|
32
|
+
|
33
|
+
# All
|
34
|
+
params.add OptionalBoolParam.new('export', 'Runs EXPORT task.')
|
35
|
+
params.add OptionalBoolParam.new('put', 'Runs PUT task.')
|
36
|
+
params.add OptionalBoolParam.new('load', 'Runs LOAD task.')
|
37
|
+
params.add OptionalBoolParam.new('gzip', 'If true, compresses target file by gzip.', default: true)
|
38
|
+
}
|
39
|
+
|
40
|
+
declarations {|params|
|
41
|
+
decls = sql_statement(params).declarations
|
42
|
+
decls.declare 'dest-table', nil
|
43
|
+
decls
|
44
|
+
}
|
45
|
+
|
46
|
+
script {|params, script|
|
47
|
+
run_all = !params['export'] && !params['put'] && !params['load']
|
48
|
+
|
49
|
+
# Export
|
50
|
+
if params['export'] || run_all
|
51
|
+
script.task(params['src-ds']) {|task|
|
52
|
+
task.export sql_statement(params),
|
53
|
+
path: params['tmp-file'],
|
54
|
+
format: 'json',
|
55
|
+
override: true,
|
56
|
+
gzip: params['gzip'],
|
57
|
+
sqldump: params['sqldump']
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
# Put
|
62
|
+
if params['put'] || run_all
|
63
|
+
script.task(params['s3-ds']) {|task|
|
64
|
+
task.put params['tmp-file'], params['s3-file'], check_args: false
|
65
|
+
}
|
66
|
+
if params['remove-tmp']
|
67
|
+
script.task(params.file_ds) {|task|
|
68
|
+
task.remove params['tmp-file']
|
69
|
+
}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Load
|
74
|
+
if params['load'] || run_all
|
75
|
+
script.task(params['dest-ds']) {|task|
|
76
|
+
prev_table = '${dest_table}_old'
|
77
|
+
work_table = '${dest_table}_wk'
|
78
|
+
|
79
|
+
task.transaction {
|
80
|
+
# CREATE
|
81
|
+
task.drop_force prev_table
|
82
|
+
task.drop_force work_table
|
83
|
+
task.exec params['table-def'].replace(/\$\{?dest_table\}?\b/, work_table)
|
84
|
+
|
85
|
+
# COPY
|
86
|
+
task.load params['s3-ds'], params['s3-file'], work_table,
|
87
|
+
'json', nil, params['options'].merge('gzip' => params['gzip'])
|
88
|
+
|
89
|
+
# GRANT, ANALYZE
|
90
|
+
task.grant_if params['grant'], work_table
|
91
|
+
task.analyze_if params['analyze'], work_table
|
92
|
+
|
93
|
+
# RENAME
|
94
|
+
task.create_dummy_table '${dest_table}'
|
95
|
+
task.rename_table params['dest-table'].to_s, "#{params['dest-table'].name}_old"
|
96
|
+
task.rename_table work_table, params['dest-table'].name
|
97
|
+
}
|
98
|
+
|
99
|
+
task.drop_force prev_table if params['no-backup']
|
100
|
+
|
101
|
+
# VACUUM: vacuum is needless for newly created table, applying vacuum after exposure is not a problem.
|
102
|
+
task.vacuum_if params['vacuum'], params['vacuum-sort'], params['dest-table'].to_s
|
103
|
+
}
|
104
|
+
end
|
105
|
+
}
|
106
|
+
|
107
|
+
def sql_statement(params)
|
108
|
+
return params['sql-file'] if params['sql-file']
|
109
|
+
srcs = params['src-tables']
|
110
|
+
raise ParameterError, "src-tables must be singleton when no sql-file is given" unless srcs.size == 1
|
111
|
+
src_table_var = srcs.keys.first
|
112
|
+
stmt = SQLStatement.for_string("select * from $#{src_table_var};")
|
113
|
+
stmt.declarations = Declarations.new({src_table_var => src_table_var})
|
114
|
+
stmt
|
115
|
+
end
|
116
|
+
}
|
@@ -0,0 +1,363 @@
|
|
1
|
+
require 'bricolage/datasource'
|
2
|
+
require 'mysql2'
|
3
|
+
require 'json'
|
4
|
+
require 'csv'
|
5
|
+
require 'stringio'
|
6
|
+
require 'open3'
|
7
|
+
|
8
|
+
module Bricolage
|
9
|
+
|
10
|
+
class MySQLDataSource < DataSource
|
11
|
+
declare_type 'mysql'
|
12
|
+
|
13
|
+
def initialize(**mysql_options)
|
14
|
+
@mysql_options = mysql_options
|
15
|
+
@client = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :mysql_options
|
19
|
+
|
20
|
+
def host
|
21
|
+
@mysql_options[:host]
|
22
|
+
end
|
23
|
+
|
24
|
+
def port
|
25
|
+
@mysql_options[:port]
|
26
|
+
end
|
27
|
+
|
28
|
+
def username
|
29
|
+
@mysql_options[:username]
|
30
|
+
end
|
31
|
+
|
32
|
+
def password
|
33
|
+
@mysql_options[:password]
|
34
|
+
end
|
35
|
+
|
36
|
+
def database
|
37
|
+
@mysql_options[:database]
|
38
|
+
end
|
39
|
+
|
40
|
+
def new_task
|
41
|
+
MySQLTask.new(self)
|
42
|
+
end
|
43
|
+
|
44
|
+
def open
|
45
|
+
@client = Mysql2::Client.new(**@mysql_options)
|
46
|
+
begin
|
47
|
+
yield self
|
48
|
+
ensure
|
49
|
+
c = @client
|
50
|
+
@client = nil
|
51
|
+
c.close
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def query(sql, **opts)
|
56
|
+
logger.info "[SQL] #{sql}"
|
57
|
+
connection_check
|
58
|
+
@client.query(sql, **opts)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def connection_check
|
64
|
+
unless @client
|
65
|
+
raise FatalError, "#{self.class} used outside of \#open block"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class MySQLTask < DataSourceTask
|
71
|
+
def export(stmt, path: nil, format: nil, override: false, gzip: false, sqldump: false)
|
72
|
+
add Export.new(stmt, path: path, format: format, override: override, gzip: gzip, sqldump: sqldump)
|
73
|
+
end
|
74
|
+
|
75
|
+
class Export < Action
|
76
|
+
def initialize(stmt, path: nil, format: nil, override: false, gzip: false, sqldump: false)
|
77
|
+
@statement = stmt
|
78
|
+
@path = path
|
79
|
+
@format = format
|
80
|
+
@override = override
|
81
|
+
@gzip = gzip
|
82
|
+
@sqldump = sqldump
|
83
|
+
end
|
84
|
+
|
85
|
+
def bind(*args)
|
86
|
+
@statement.bind(*args)
|
87
|
+
end
|
88
|
+
|
89
|
+
def source
|
90
|
+
@statement.stripped_source
|
91
|
+
end
|
92
|
+
|
93
|
+
def run
|
94
|
+
if @sqldump and sqldump_available? and sqldump_usable?
|
95
|
+
export_by_sqldump
|
96
|
+
else
|
97
|
+
export_by_ruby
|
98
|
+
end
|
99
|
+
JobResult.success
|
100
|
+
end
|
101
|
+
|
102
|
+
def export_by_sqldump
|
103
|
+
cmds = [[{"SQLDUMP_PASSWORD" => ds.password}, sqldump_path.to_s, "--#{@format}", ds.host, ds.port.to_s, ds.username, ds.database, @statement.stripped_source]]
|
104
|
+
cmds.push [GZIP_COMMAND] if @gzip
|
105
|
+
cmds.last.push({out: @path.to_s})
|
106
|
+
ds.logger.info '[CMD] ' + format_pipeline(cmds)
|
107
|
+
statuses = Open3.pipeline(*cmds)
|
108
|
+
statuses.each_with_index do |st, idx|
|
109
|
+
unless st.success?
|
110
|
+
cmd = cmds[idx].first
|
111
|
+
raise JobFailure, "sqldump failed (status #{st.to_i})"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def format_pipeline(cmds)
|
117
|
+
cmds = cmds.map {|args| args[0].kind_of?(Hash) ? args[1..-1] : args.dup } # do not show env
|
118
|
+
cmds.map {|args| %Q("#{args.join('" "')}") }.join(' | ')
|
119
|
+
end
|
120
|
+
|
121
|
+
def sqldump_available?
|
122
|
+
sqldump_real_path.executable?
|
123
|
+
end
|
124
|
+
|
125
|
+
def sqldump_path
|
126
|
+
Pathname(__dir__).parent.parent + "libexec/sqldump"
|
127
|
+
end
|
128
|
+
|
129
|
+
def sqldump_real_path
|
130
|
+
Pathname("#{sqldump_path}.#{platform_name}")
|
131
|
+
end
|
132
|
+
|
133
|
+
def platform_name
|
134
|
+
@platform_name ||= `uname -s`.strip
|
135
|
+
end
|
136
|
+
|
137
|
+
def sqldump_usable?
|
138
|
+
%w[json tsv].include?(@format)
|
139
|
+
end
|
140
|
+
|
141
|
+
def export_by_ruby
|
142
|
+
ds.logger.info "exporting table into #{@path} ..."
|
143
|
+
count = 0
|
144
|
+
open_target_file(@path) {|f|
|
145
|
+
writer_class = WRITER_CLASSES[@format] or raise ArgumentError, "unknown export format: #{@format.inspect}"
|
146
|
+
writer = writer_class.new(f)
|
147
|
+
rs = ds.query(@statement.stripped_source, as: writer_class.record_format, stream: true, cache_rows: false)
|
148
|
+
ds.logger.info "got result set, writing..."
|
149
|
+
rs.each do |values|
|
150
|
+
writer.write_record values
|
151
|
+
count += 1
|
152
|
+
ds.logger.info "#{count} records exported..." if count % 10_0000 == 0
|
153
|
+
end
|
154
|
+
}
|
155
|
+
ds.logger.info "#{count} records exported; export finished"
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
# FIXME: parameterize
|
161
|
+
GZIP_COMMAND = 'gzip'
|
162
|
+
|
163
|
+
def open_target_file(path, &block)
|
164
|
+
unless @override
|
165
|
+
raise JobFailure, "destination file already exists: #{path}" if File.exist?(path)
|
166
|
+
end
|
167
|
+
if @gzip
|
168
|
+
ds.logger.info "enable compression: gzip"
|
169
|
+
IO.popen(%Q(#{GZIP_COMMAND} > "#{path}"), 'w', &block)
|
170
|
+
else
|
171
|
+
File.open(path, 'w', &block)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def s3export(table, stmt, s3ds, prefix, gzip, dump_options)
|
177
|
+
options = dump_options.nil? ? {} : dump_options[:dump_options]
|
178
|
+
add S3Export.new(table, stmt, s3ds, prefix, gzip: gzip,
|
179
|
+
format: options['format'],
|
180
|
+
partition_column: options['partition_column'],
|
181
|
+
partition_number: options['partition_number'],
|
182
|
+
write_concurrency: options['write_concurrency'],
|
183
|
+
rotation_size: options['rotation_size'],
|
184
|
+
delete_objects: options['delete_objects'],
|
185
|
+
object_key_delimiter: options['object_key_delimiter'],
|
186
|
+
src_zone_offset: options['src_zone_offset'],
|
187
|
+
dst_zone_offset: options['dst_zone_offset'])
|
188
|
+
end
|
189
|
+
|
190
|
+
class S3Export < Action
|
191
|
+
|
192
|
+
def initialize(table, stmt, s3ds, prefix, gzip: true,
|
193
|
+
format: "json",
|
194
|
+
partition_column: nil,
|
195
|
+
partition_number: 4,
|
196
|
+
write_concurrency: 4,
|
197
|
+
rotation_size: nil,
|
198
|
+
delete_objects: false,
|
199
|
+
object_key_delimiter: nil,
|
200
|
+
src_zone_offset: nil,
|
201
|
+
dst_zone_offset: nil)
|
202
|
+
@table = table
|
203
|
+
@statement = stmt
|
204
|
+
@s3ds = s3ds
|
205
|
+
@prefix = build_prefix @s3ds.prefix, prefix
|
206
|
+
@format = format
|
207
|
+
@gzip = gzip
|
208
|
+
@partition_column = partition_column
|
209
|
+
@partition_number = partition_number
|
210
|
+
@write_concurrency = write_concurrency
|
211
|
+
@rotation_size = rotation_size
|
212
|
+
@delete_objects = delete_objects
|
213
|
+
@object_key_delimiter = object_key_delimiter
|
214
|
+
@src_zone_offset = src_zone_offset
|
215
|
+
@dst_zone_offset = dst_zone_offset
|
216
|
+
end
|
217
|
+
|
218
|
+
def run
|
219
|
+
s3export
|
220
|
+
JobResult.success
|
221
|
+
end
|
222
|
+
|
223
|
+
def bind(*args)
|
224
|
+
@statement.bind(*args) if @statement
|
225
|
+
end
|
226
|
+
|
227
|
+
def source
|
228
|
+
"-- myexport #{@table} -> #{@s3ds.bucket_name}/#{@prefix}" +
|
229
|
+
(@statement ? "\n#{@statement.stripped_source}" : "")
|
230
|
+
end
|
231
|
+
|
232
|
+
def s3export
|
233
|
+
cmd = build_cmd(command_parameters)
|
234
|
+
ds.logger.info "[CMD] #{cmd}"
|
235
|
+
out, st = Open3.capture2e(environment_variables, cmd)
|
236
|
+
ds.logger.info "[CMDOUT] #{out}"
|
237
|
+
unless st.success?
|
238
|
+
msg = extract_exception_message(out)
|
239
|
+
raise JobFailure, "mys3dump failed (status: #{st.to_i}): #{msg}"
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def environment_variables
|
244
|
+
{
|
245
|
+
'AWS_ACCESS_KEY_ID' => @s3ds.access_key,
|
246
|
+
'AWS_SECRET_ACCESS_KEY' => @s3ds.secret_key,
|
247
|
+
'MYS3DUMP_PASSWORD' => ds.password
|
248
|
+
}
|
249
|
+
end
|
250
|
+
|
251
|
+
def command_parameters
|
252
|
+
params = {
|
253
|
+
jar: mys3dump_path.to_s,
|
254
|
+
h: ds.host,
|
255
|
+
P: ds.port.to_s,
|
256
|
+
D: ds.database,
|
257
|
+
u: ds.username,
|
258
|
+
#p: ds.password,
|
259
|
+
o: connection_property,
|
260
|
+
t: @table,
|
261
|
+
b: @s3ds.bucket.name,
|
262
|
+
x: @prefix
|
263
|
+
}
|
264
|
+
params[:q] = @statement.stripped_source.chomp(';') if @statement
|
265
|
+
params[:f] = @format if @format
|
266
|
+
params[:C] = nil if @gzip
|
267
|
+
params[:c] = @partition_column if @partition_column
|
268
|
+
params[:n] = @partition_number if @partition_number
|
269
|
+
params[:w] = @write_concurrency if @write_concurrency
|
270
|
+
params[:r] = @rotation_size if @rotation_size
|
271
|
+
params[:d] = nil if @delete_objects
|
272
|
+
params[:k] = @object_key_delimiter if @object_key_delimiter
|
273
|
+
if src_zone_offset = @src_zone_offset || ds.mysql_options[:src_zone_offset]
|
274
|
+
params[:S] = src_zone_offset
|
275
|
+
end
|
276
|
+
if dst_zone_offset = @dst_zone_offset || ds.mysql_options[:dst_zone_offset]
|
277
|
+
params[:T] = dst_zone_offset
|
278
|
+
end
|
279
|
+
params
|
280
|
+
end
|
281
|
+
|
282
|
+
OPTION_MAP = {
|
283
|
+
encoding: 'useUnicode=true&characterEncoding',
|
284
|
+
read_timeout: 'netTimeoutForStreamingResults',
|
285
|
+
connect_timeout: 'connectTimeout',
|
286
|
+
reconnect: 'autoReconnect',
|
287
|
+
collation: 'connectionCollation'
|
288
|
+
}
|
289
|
+
|
290
|
+
def connection_property
|
291
|
+
ds.mysql_options.map {|k, v| opt = OPTION_MAP[k] ; opt ? "#{opt}=#{v}" : nil }.compact.join('&')
|
292
|
+
end
|
293
|
+
|
294
|
+
def build_prefix(ds_prefix, pm_prefix)
|
295
|
+
((ds_prefix || "") + "//" + (pm_prefix.to_s || "")).gsub(%r<\A/>, '').gsub(%r<//>, '/')
|
296
|
+
end
|
297
|
+
|
298
|
+
def mys3dump_path
|
299
|
+
Pathname(__dir__).parent.parent + "libexec/mys3dump.jar"
|
300
|
+
end
|
301
|
+
|
302
|
+
def build_cmd(options)
|
303
|
+
(['java'] + options.flat_map {|k, v| v ? ["-#{k}", v.to_s] : ["-#{k}"] }.map {|o| %Q("#{o}") }).join(" ")
|
304
|
+
end
|
305
|
+
|
306
|
+
def extract_exception_message(out)
|
307
|
+
out.lines do |line|
|
308
|
+
if /^.*Exception: (?<msg>.*)$/ =~ line
|
309
|
+
return msg
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
WRITER_CLASSES = {}
|
316
|
+
|
317
|
+
class JSONWriter
|
318
|
+
def JSONWriter.record_format
|
319
|
+
:hash
|
320
|
+
end
|
321
|
+
|
322
|
+
def initialize(f)
|
323
|
+
@f = f
|
324
|
+
end
|
325
|
+
|
326
|
+
def write_record(values)
|
327
|
+
@f.puts JSON.dump(values)
|
328
|
+
end
|
329
|
+
end
|
330
|
+
WRITER_CLASSES['json'] = JSONWriter
|
331
|
+
|
332
|
+
class TSVWriter
|
333
|
+
def TSVWriter.record_format
|
334
|
+
:array
|
335
|
+
end
|
336
|
+
|
337
|
+
def initialize(f)
|
338
|
+
@f = f
|
339
|
+
end
|
340
|
+
|
341
|
+
def write_record(values)
|
342
|
+
@f.puts values.join("\t")
|
343
|
+
end
|
344
|
+
end
|
345
|
+
WRITER_CLASSES['tsv'] = TSVWriter
|
346
|
+
|
347
|
+
class CSVWriter
|
348
|
+
def CSVWriter.record_format
|
349
|
+
:array
|
350
|
+
end
|
351
|
+
|
352
|
+
def initialize(f)
|
353
|
+
@csv = CSV.new(f)
|
354
|
+
end
|
355
|
+
|
356
|
+
def write_record(values)
|
357
|
+
@csv.add_row values
|
358
|
+
end
|
359
|
+
end
|
360
|
+
WRITER_CLASSES['csv'] = CSVWriter
|
361
|
+
end
|
362
|
+
|
363
|
+
end
|
Binary file
|
data/libexec/sqldump
ADDED
Binary file
|
Binary file
|
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bricolage-mysql
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 5.26.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Minero Aoki
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-01-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bricolage
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.26.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.26.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: mysql2
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description:
|
42
|
+
email: aamine@loveruby.net
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- README.md
|
48
|
+
- RELEASE.md
|
49
|
+
- jobclass/my-export.rb
|
50
|
+
- jobclass/my-import-delta.rb
|
51
|
+
- jobclass/my-import.rb
|
52
|
+
- jobclass/my-migrate.rb
|
53
|
+
- lib/bricolage-mysql.rb
|
54
|
+
- lib/bricolage/mysqldatasource.rb
|
55
|
+
- libexec/mys3dump.jar
|
56
|
+
- libexec/sqldump
|
57
|
+
- libexec/sqldump.Darwin
|
58
|
+
- libexec/sqldump.Linux
|
59
|
+
homepage: https://github.com/bricolages/bricolage-mysql
|
60
|
+
licenses:
|
61
|
+
- MIT
|
62
|
+
metadata: {}
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: 2.2.0
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
requirements: []
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 2.6.11
|
80
|
+
signing_key:
|
81
|
+
specification_version: 4
|
82
|
+
summary: MySQL-related job classes for Bricolage batch framework
|
83
|
+
test_files: []
|