bricolage-mysql 5.26.0

Sign up to get free protection for your applications and to get access to all the features.
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,5 @@
1
+ # bricolage-mysql gem release note
2
+
3
+ ## version 5.26.0
4
+
5
+ - first release.
@@ -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
@@ -0,0 +1,5 @@
1
+ require 'bricolage/jobclass'
2
+ require 'pathname'
3
+
4
+ jobclass_path = Pathname(__dir__).realpath.parent.cleanpath + 'jobclass'
5
+ Bricolage::JobClass.add_load_path jobclass_path
Binary file
data/libexec/sqldump ADDED
@@ -0,0 +1,9 @@
1
+ #!/bin/sh
2
+
3
+ binary="$0.$(uname -s)"
4
+ if ! [[ -x $binary ]]
5
+ then
6
+ echo "$0: error: sqldump does not support $(uname -s)" 1>&2
7
+ exit 1
8
+ fi
9
+ exec "$binary" "$@"
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: []