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 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: []