bricolage-mysql 5.26.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 +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: []
|