bricolage 5.8.7
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 +4 -0
- data/bin/bricolage +6 -0
- data/bin/bricolage-jobnet +6 -0
- data/jobclass/create.rb +21 -0
- data/jobclass/exec.rb +17 -0
- data/jobclass/insert-delta.rb +31 -0
- data/jobclass/insert.rb +33 -0
- data/jobclass/load.rb +39 -0
- data/jobclass/my-export.rb +40 -0
- data/jobclass/my-migrate.rb +103 -0
- data/jobclass/noop.rb +13 -0
- data/jobclass/rebuild-drop.rb +37 -0
- data/jobclass/rebuild-rename.rb +49 -0
- data/jobclass/s3-put.rb +19 -0
- data/jobclass/sql.rb +29 -0
- data/jobclass/td-delete.rb +20 -0
- data/jobclass/td-export.rb +30 -0
- data/jobclass/unload.rb +30 -0
- data/jobclass/wait-file.rb +48 -0
- data/lib/bricolage/application.rb +260 -0
- data/lib/bricolage/commandutils.rb +52 -0
- data/lib/bricolage/configloader.rb +126 -0
- data/lib/bricolage/context.rb +108 -0
- data/lib/bricolage/datasource.rb +144 -0
- data/lib/bricolage/eventhandlers.rb +47 -0
- data/lib/bricolage/exception.rb +47 -0
- data/lib/bricolage/filedatasource.rb +42 -0
- data/lib/bricolage/filesystem.rb +165 -0
- data/lib/bricolage/genericdatasource.rb +37 -0
- data/lib/bricolage/job.rb +212 -0
- data/lib/bricolage/jobclass.rb +98 -0
- data/lib/bricolage/jobfile.rb +100 -0
- data/lib/bricolage/jobflow.rb +389 -0
- data/lib/bricolage/jobnetrunner.rb +264 -0
- data/lib/bricolage/jobresult.rb +74 -0
- data/lib/bricolage/logger.rb +52 -0
- data/lib/bricolage/mysqldatasource.rb +223 -0
- data/lib/bricolage/parameters.rb +653 -0
- data/lib/bricolage/postgresconnection.rb +78 -0
- data/lib/bricolage/psqldatasource.rb +449 -0
- data/lib/bricolage/resource.rb +68 -0
- data/lib/bricolage/rubyjobclass.rb +42 -0
- data/lib/bricolage/s3datasource.rb +144 -0
- data/lib/bricolage/script.rb +120 -0
- data/lib/bricolage/sqlstatement.rb +351 -0
- data/lib/bricolage/taskqueue.rb +156 -0
- data/lib/bricolage/tddatasource.rb +116 -0
- data/lib/bricolage/variables.rb +208 -0
- data/lib/bricolage/version.rb +4 -0
- data/lib/bricolage.rb +8 -0
- data/libexec/sqldump +9 -0
- data/libexec/sqldump.Darwin +0 -0
- data/libexec/sqldump.Linux +0 -0
- data/test/all.rb +3 -0
- data/test/home/config/development/database.yml +57 -0
- data/test/home/config/development/password.yml +2 -0
- data/test/home/subsys/separated.job +1 -0
- data/test/home/subsys/separated.sql +1 -0
- data/test/home/subsys/unified.jobnet +1 -0
- data/test/home/subsys/unified.sql.job +5 -0
- data/test/test_filesystem.rb +19 -0
- data/test/test_parameters.rb +401 -0
- data/test/test_variables.rb +114 -0
- metadata +192 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
require 'bricolage/variables'
|
|
2
|
+
require 'bricolage/sqlstatement'
|
|
3
|
+
require 'bricolage/exception'
|
|
4
|
+
require 'date'
|
|
5
|
+
require 'pathname'
|
|
6
|
+
|
|
7
|
+
module Bricolage
|
|
8
|
+
|
|
9
|
+
class Parameters
|
|
10
|
+
|
|
11
|
+
# Job parameter declarations defined by job class.
|
|
12
|
+
class Declarations
|
|
13
|
+
def initialize
|
|
14
|
+
@decls = {} # {name => Param}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def inspect
|
|
18
|
+
"\#<#{self.class} #{@decls.inspect}>"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def add(param)
|
|
22
|
+
@decls[param.name] = param
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def [](key)
|
|
26
|
+
@decls[key]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def keys
|
|
30
|
+
@decls.keys
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def each(&block)
|
|
34
|
+
@decls.each_value(&block)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parse_direct_values(values)
|
|
38
|
+
DirectValueHandler.new(self).parse(values)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def parsing_options
|
|
42
|
+
h = CommandLineOptionHandler.new(self)
|
|
43
|
+
yield h
|
|
44
|
+
h.values
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def union_intermediate_values(*ival_list)
|
|
48
|
+
IntermediateValues.union(self, *ival_list)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Handles *.job file values.
|
|
53
|
+
# Declarations + values -> IntermediateValues
|
|
54
|
+
class DirectValueHandler
|
|
55
|
+
def initialize(decls)
|
|
56
|
+
@decls = decls # Declarations
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# values :: {String => a}
|
|
60
|
+
def parse(values)
|
|
61
|
+
parsed_values = {}
|
|
62
|
+
vars = Variables.new
|
|
63
|
+
values.each do |name, value|
|
|
64
|
+
if decl = @decls[name]
|
|
65
|
+
val = decl.parse_value(value)
|
|
66
|
+
# nil is equal to "no option given" semantically
|
|
67
|
+
parsed_values[name] = val unless val.nil?
|
|
68
|
+
else
|
|
69
|
+
vars.add Variable.new(name, value)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
IntermediateValues.new(@decls, parsed_values, vars)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Handles param values given by command line options (e.g. --dest-table=t).
|
|
77
|
+
# Declarations + option_args -> IntermediateValues
|
|
78
|
+
class CommandLineOptionHandler
|
|
79
|
+
def initialize(decls)
|
|
80
|
+
@decls = decls # Declarations
|
|
81
|
+
@values = {} # {name => a}
|
|
82
|
+
@vars = Variables.new
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def define_options(parser)
|
|
86
|
+
@decls.each do |decl|
|
|
87
|
+
desc = (decl.optional? ? '[optional] ' : '') + decl.description
|
|
88
|
+
arg_spec = decl.have_arg? ? "=#{decl.arg_spec}" : ''
|
|
89
|
+
parser.on("--#{decl.option_name}#{arg_spec}", desc) {|arg|
|
|
90
|
+
@values[decl.name] = decl.parse_option_value(arg, @values[decl.name])
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
parser.on('-v', '--variable=NAME=VALUE', 'Set variable.') {|name_value|
|
|
94
|
+
name, value = name_value.split('=', 2)
|
|
95
|
+
@vars.add Variable.new(name, value)
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def values
|
|
100
|
+
IntermediateValues.new(@decls, @values, @vars)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Unified intermediate param values representation.
|
|
105
|
+
# This class is used for both of *.job file and options.
|
|
106
|
+
class IntermediateValues
|
|
107
|
+
def IntermediateValues.union(decls, *vals_list)
|
|
108
|
+
result = empty(decls)
|
|
109
|
+
vals_list.each do |vals|
|
|
110
|
+
result.update vals
|
|
111
|
+
end
|
|
112
|
+
result
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def IntermediateValues.empty(decls)
|
|
116
|
+
new(decls, {}, Variables.new)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def initialize(decls, values, vars)
|
|
120
|
+
@decls = decls # Declarations
|
|
121
|
+
@values = values # {name => a}
|
|
122
|
+
@variables = vars # Variables
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
attr_reader :decls
|
|
126
|
+
attr_reader :values
|
|
127
|
+
attr_reader :variables
|
|
128
|
+
|
|
129
|
+
def [](name)
|
|
130
|
+
@values[name]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def []=(name, value)
|
|
134
|
+
@values[name] = value
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def keys
|
|
138
|
+
@values.keys
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def update(other)
|
|
142
|
+
unless @decls == other.decls
|
|
143
|
+
raise "[BUG] merging IntermediateValues with different paramdecl: #{self.inspect} - #{other.inspect}"
|
|
144
|
+
end
|
|
145
|
+
@values.update other.values
|
|
146
|
+
@variables.update other.variables
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def resolve(ctx, vars)
|
|
150
|
+
materialized = materialize(ctx, vars)
|
|
151
|
+
resolved = resolved_variables(materialized, vars)
|
|
152
|
+
Parameters.new(@decls, materialized, resolved, ctx)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
def materialize(ctx, vars)
|
|
158
|
+
h = {}
|
|
159
|
+
@decls.each do |decl|
|
|
160
|
+
value = @values[decl.name]
|
|
161
|
+
# value==nil means "no parameter given" or "no option given".
|
|
162
|
+
# Note that false is a *valid* value, "falthy" check is not sufficient here.
|
|
163
|
+
if value.nil?
|
|
164
|
+
raise ParameterError, "parameter not given: #{decl.name}" if decl.required?
|
|
165
|
+
h[decl.name] = decl.default_value(ctx, vars)
|
|
166
|
+
else
|
|
167
|
+
h[decl.name] = decl.materialize(value, ctx, vars)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
h
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def resolved_variables(materialized, base_vars)
|
|
174
|
+
Variables.define {|vars|
|
|
175
|
+
materialized.each do |name, value|
|
|
176
|
+
decl = @decls[name]
|
|
177
|
+
next unless decl.publish?
|
|
178
|
+
next if value.nil?
|
|
179
|
+
decl.variables(value).each do |var|
|
|
180
|
+
vars.add var
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
vars.update @variables
|
|
184
|
+
}.resolve_with(base_vars)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# class Parameters
|
|
189
|
+
# Materialized, fixed job parameter values.
|
|
190
|
+
|
|
191
|
+
def initialize(decls, values, vars, ctx)
|
|
192
|
+
@decls = decls # Declarations
|
|
193
|
+
@values = values # {name => a}
|
|
194
|
+
@variables = vars # ResolvedVariables
|
|
195
|
+
@context = ctx # Context
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def inspect
|
|
199
|
+
"\#<#{self.class} #{@values.inspect}>"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def [](key)
|
|
203
|
+
raise ParameterError, "no such parameter: #{key}" unless @values.key?(key)
|
|
204
|
+
@values[key]
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
alias get []
|
|
208
|
+
|
|
209
|
+
def keys
|
|
210
|
+
@values.keys
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
attr_reader :variables
|
|
214
|
+
|
|
215
|
+
# FIXME: remove
|
|
216
|
+
def generic_ds
|
|
217
|
+
@context.get_data_source('generic', 'generic')
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# FIXME: remove
|
|
221
|
+
def file_ds
|
|
222
|
+
@context.get_data_source('file', 'file')
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
end # class Parameters
|
|
226
|
+
|
|
227
|
+
class Param
|
|
228
|
+
def initialize(name, arg_spec, description, optional: false, publish: false)
|
|
229
|
+
@name = name
|
|
230
|
+
@arg_spec = arg_spec
|
|
231
|
+
@description = description
|
|
232
|
+
@optional = optional
|
|
233
|
+
@publish = publish
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
attr_reader :name
|
|
237
|
+
attr_reader :description
|
|
238
|
+
|
|
239
|
+
def option_name
|
|
240
|
+
name
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
attr_reader :arg_spec
|
|
244
|
+
|
|
245
|
+
def have_arg?
|
|
246
|
+
!!@arg_spec
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def optional?
|
|
250
|
+
@optional
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def required?
|
|
254
|
+
not optional?
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# "published" parameter defines SQL variable.
|
|
258
|
+
def publish?
|
|
259
|
+
@publish
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def inspect
|
|
263
|
+
attrs = [
|
|
264
|
+
(@optional ? 'optional' : 'required'),
|
|
265
|
+
(@publish ? 'publish' : nil)
|
|
266
|
+
].compact.join(',')
|
|
267
|
+
"\#<#{self.class} #{name} #{attrs}>"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
#
|
|
271
|
+
# Value Handling
|
|
272
|
+
#
|
|
273
|
+
|
|
274
|
+
def parse_option_value(arg, acc)
|
|
275
|
+
return nil if arg.nil?
|
|
276
|
+
arg
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def parse_value(value)
|
|
280
|
+
value
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# abstract def default_value(ctx, vars)
|
|
284
|
+
# abstract def materialize(value, ctx, vars)
|
|
285
|
+
# abstract def variables(value)
|
|
286
|
+
|
|
287
|
+
private
|
|
288
|
+
|
|
289
|
+
def expand(str, vars)
|
|
290
|
+
Variable.expand_string(str.to_s) {|var|
|
|
291
|
+
vars[var] or raise ParameterError, "undefined variable in parameter #{name}: #{var}"
|
|
292
|
+
}
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def wrap_variable_value(val)
|
|
296
|
+
[ResolvedVariable.new(name.gsub('-', '_'), val)]
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
class StringParam < Param
|
|
301
|
+
def initialize(name, arg_spec, description, optional: false, publish: false)
|
|
302
|
+
super name, arg_spec, description, optional: optional, publish: publish
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def default_value(ctx, vars)
|
|
306
|
+
nil
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def materialize(value, ctx, vars)
|
|
310
|
+
expand(value, vars)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def variables(value)
|
|
314
|
+
wrap_variable_value(value)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
class BoolParam < Param
|
|
319
|
+
def initialize(name, description, publish: false)
|
|
320
|
+
super name, nil, description, publish: publish
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def default_value(ctx, vars)
|
|
324
|
+
false
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def materialize(value, ctx, vars)
|
|
328
|
+
!!value
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def variables(bool)
|
|
332
|
+
wrap_variable_value(bool.to_s)
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
class OptionalBoolParam < Param
|
|
337
|
+
def initialize(name, description, default: false, publish: false)
|
|
338
|
+
super name, nil, description, optional: true, publish: publish
|
|
339
|
+
@default_value = default
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def default_value(ctx, vars)
|
|
343
|
+
@default_value
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def materialize(value, ctx, vars)
|
|
347
|
+
!!value
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def variables(bool)
|
|
351
|
+
wrap_variable_value(bool.to_s)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
class DateParam < Param
|
|
356
|
+
def initialize(name, arg_spec, description, optional: false, publish: false)
|
|
357
|
+
super name, arg_spec, description, optional: optional, publish: publish
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def default_value(ctx, vars)
|
|
361
|
+
nil
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def materialize(value, ctx, vars)
|
|
365
|
+
case value
|
|
366
|
+
when Date
|
|
367
|
+
value
|
|
368
|
+
when String
|
|
369
|
+
begin
|
|
370
|
+
Date.parse(expand(value, vars))
|
|
371
|
+
rescue ArgumentError
|
|
372
|
+
raise ParameterError, "bad date format: #{value.inspect}"
|
|
373
|
+
end
|
|
374
|
+
else
|
|
375
|
+
raise ParameterError, "unknown type for date parameter '#{name}': #{value.class}"
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def variables(date)
|
|
380
|
+
# "YYYY-MM-DD"
|
|
381
|
+
wrap_variable_value(date.to_s)
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
class EnumParam < Param
|
|
386
|
+
def initialize(name, list, description, default: nil, publish: false)
|
|
387
|
+
super name, 'VALUE', description, optional: (default ? true : false), publish: publish
|
|
388
|
+
@list = list.map {|val| val.to_s.freeze }
|
|
389
|
+
@default_value = default
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def description
|
|
393
|
+
"#{super} (#{@list.join(', ')})" + (@default ? " [default: #{@default}]" : '')
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def default_value(ctx, vars)
|
|
397
|
+
@default_value
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def materialize(value, ctx, vars)
|
|
401
|
+
val = expand(value.to_s, vars)
|
|
402
|
+
unless @list.include?(val)
|
|
403
|
+
raise ParameterError, "unknown value for enum parameter '#{name}': #{val.inspect}"
|
|
404
|
+
end
|
|
405
|
+
val
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def variables(val)
|
|
409
|
+
wrap_variable_value(val)
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
class DataSourceParam < Param
|
|
414
|
+
def initialize(kind, name = 'data-source', description = 'Main data source.', optional: true, publish: false)
|
|
415
|
+
raise FatalError, "no data source kind declared" unless kind
|
|
416
|
+
super name, 'NAME', description, optional: optional, publish: publish
|
|
417
|
+
@kind = kind
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def description
|
|
421
|
+
"#{super} [default: #{@kind}]"
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def default_value(ctx, vars)
|
|
425
|
+
ctx.get_data_source(@kind, nil)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def materialize(value, ctx, vars)
|
|
429
|
+
ctx.get_data_source(@kind, expand(value, vars))
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def variables(ds)
|
|
433
|
+
wrap_variable_value(ds.name)
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
class SQLFileParam < Param
|
|
438
|
+
def initialize(name = 'sql-file', arg_spec = 'PATH', description = 'SQL file.', optional: false, publish: false)
|
|
439
|
+
super name, arg_spec, description, optional: optional, publish: publish
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def default_value(ctx, vars)
|
|
443
|
+
nil
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def materialize(name_or_stmt, ctx, vars)
|
|
447
|
+
case name_or_stmt
|
|
448
|
+
when String
|
|
449
|
+
name = name_or_stmt
|
|
450
|
+
SQLStatement.new(ctx.parameter_file(expand(name.to_s, vars), 'sql'))
|
|
451
|
+
when SQLStatement
|
|
452
|
+
name_or_stmt
|
|
453
|
+
else
|
|
454
|
+
raise ParameterError, "unknown type for parameter #{name}: #{name_or_stmt.class}"
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def variables(stmt)
|
|
459
|
+
wrap_variable_value(stmt.location)
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
class DestTableParam < Param
|
|
464
|
+
def initialize(
|
|
465
|
+
name = 'dest-table',
|
|
466
|
+
arg_spec = '[SCHEMA.]TABLE',
|
|
467
|
+
description = 'Target table name.',
|
|
468
|
+
# [CLUDGE] Default dest_table is provided by SQL parameter declarations,
|
|
469
|
+
# we cannot require the value here. I know this is bad...
|
|
470
|
+
optional: true,
|
|
471
|
+
publish: true
|
|
472
|
+
)
|
|
473
|
+
super name, arg_spec, description, optional: optional, publish: publish
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def default_value(ctx, vars)
|
|
477
|
+
nil
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def materialize(spec, ctx, vars)
|
|
481
|
+
TableSpec.parse(expand(spec, vars))
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def variables(spec)
|
|
485
|
+
wrap_variable_value(spec.to_s)
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
class SrcTableParam < Param
|
|
490
|
+
def initialize(
|
|
491
|
+
name = 'src-tables',
|
|
492
|
+
arg_spec = 'VAR:[SCHEMA.]TABLE',
|
|
493
|
+
description = 'Source table name.',
|
|
494
|
+
optional: true, # Source tables may not exist (e.g. data loading)
|
|
495
|
+
publish: true
|
|
496
|
+
)
|
|
497
|
+
super name, arg_spec, description, optional: optional, publish: publish
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def option_name
|
|
501
|
+
'src-table'
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def parse_option_value(value, h)
|
|
505
|
+
var, spec = value.split(':', 2)
|
|
506
|
+
if not var or var.empty?
|
|
507
|
+
raise ParameterError, "missing variable name: #{value.inspect}"
|
|
508
|
+
end
|
|
509
|
+
(h ||= {})[var] = spec
|
|
510
|
+
h # accumulator
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def parse_value(h)
|
|
514
|
+
raise ParameterError, "bad type for parameter #{name}: #{h.class}" unless h.kind_of?(Hash)
|
|
515
|
+
h.empty? ? nil : h
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def default_value(ctx, vars)
|
|
519
|
+
{}
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def materialize(h, ctx, vars)
|
|
523
|
+
map = {}
|
|
524
|
+
h.each do |name, spec|
|
|
525
|
+
map[name] = TableSpec.parse(expand(spec, vars))
|
|
526
|
+
end
|
|
527
|
+
map
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def variables(h)
|
|
531
|
+
h.map {|name, value| ResolvedVariable.new(name, value.to_s) }
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
class DestFileParam < Param
|
|
536
|
+
def initialize(name = 'dest-file', arg_spec = 'PATH', description = 'Target file name.',
|
|
537
|
+
optional: false, publish: false)
|
|
538
|
+
super name, arg_spec, description, optional: optional, publish: publish
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def default_value(ctx, vars)
|
|
542
|
+
nil
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def materialize(path, ctx, vars)
|
|
546
|
+
Pathname(expand(path, vars))
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def variables(path)
|
|
550
|
+
wrap_variable_value(path.to_s)
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
class SrcFileParam < Param
|
|
555
|
+
def initialize(name = 'src-file', arg_spec = 'PATH', description = 'Source file name.',
|
|
556
|
+
optional: false, publish: false)
|
|
557
|
+
super name, arg_spec, description, optional: optional, publish: publish
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def default_value(ctx, vars)
|
|
561
|
+
nil
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def materialize(path, ctx, vars)
|
|
565
|
+
Pathname(expand(path, vars))
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def variables(path)
|
|
569
|
+
wrap_variable_value(path.to_s)
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
class StringListParam < Param
|
|
574
|
+
def initialize(name, arg_spec, description, optional: false, publish: false)
|
|
575
|
+
super name, arg_spec, description, optional: optional, publish: publish
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def parse_option_value(value, list)
|
|
579
|
+
(list ||= []).push value
|
|
580
|
+
list # accumulator
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def parse_value(vals)
|
|
584
|
+
raise ParameterError, "bad type for parameter #{name}: #{vals.class}" unless vals.kind_of?(Array)
|
|
585
|
+
vals.empty? ? nil : vals
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def default_value(ctx, vars)
|
|
589
|
+
[]
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def materialize(vals, ctx, vars)
|
|
593
|
+
vals.map {|val| expand(val, vars) }
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def variables(strs)
|
|
597
|
+
wrap_variable_value(strs.join(' '))
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
class KeyValuePairsParam < Param
|
|
602
|
+
def initialize(name, arg_spec, description, optional: true, default: nil, value_handler: nil)
|
|
603
|
+
super name, arg_spec, description, optional: optional, publish: false
|
|
604
|
+
@default_value = default
|
|
605
|
+
@value_handler = value_handler
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def parse_option_value(value, h)
|
|
609
|
+
var, spec = value.split(':', 2)
|
|
610
|
+
if not var or var.empty?
|
|
611
|
+
raise ParameterError, "missing variable name: #{value.inspect}"
|
|
612
|
+
end
|
|
613
|
+
(h ||= {})[var] = spec
|
|
614
|
+
h # accumulator
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def parse_value(h)
|
|
618
|
+
case h
|
|
619
|
+
when Hash
|
|
620
|
+
h.empty? ? nil : h
|
|
621
|
+
when String # FIXME: should be removed after changing all load options
|
|
622
|
+
raise ParameterError, "bad type for parameter #{name}: #{h.class}" unless @value_handler
|
|
623
|
+
h.strip.empty? ? nil : h
|
|
624
|
+
else
|
|
625
|
+
raise ParameterError, "bad type for parameter #{name}: #{h.class}"
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def default_value(ctx, vars)
|
|
630
|
+
@default_value
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def materialize(pairs, ctx, vars)
|
|
634
|
+
if @value_handler
|
|
635
|
+
@value_handler.call(pairs, ctx, vars)
|
|
636
|
+
else
|
|
637
|
+
unless pairs.kind_of?(Hash)
|
|
638
|
+
raise "[BUG] bad value type #{pairs.class} for KeyValuePairsParam\#materialize (#{name})"
|
|
639
|
+
end
|
|
640
|
+
h = {}
|
|
641
|
+
pairs.each do |name, value|
|
|
642
|
+
h[name] = expand(value.to_s, vars)
|
|
643
|
+
end
|
|
644
|
+
h
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def variables(h)
|
|
649
|
+
[]
|
|
650
|
+
end
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require 'bricolage/exception'
|
|
2
|
+
require 'pg'
|
|
3
|
+
|
|
4
|
+
module Bricolage
|
|
5
|
+
|
|
6
|
+
class PostgreSQLException < SQLException; end
|
|
7
|
+
|
|
8
|
+
class PostgresConnection
|
|
9
|
+
def initialize(connection, ds, logger)
|
|
10
|
+
@connection = connection
|
|
11
|
+
@ds = ds
|
|
12
|
+
@logger = logger
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def source
|
|
16
|
+
@connection
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def execute(query)
|
|
20
|
+
@logger.info "[#{@ds.name}] #{query}"
|
|
21
|
+
log_elapsed_time {
|
|
22
|
+
rs = @connection.exec(query)
|
|
23
|
+
result = rs.to_a
|
|
24
|
+
rs.clear
|
|
25
|
+
result
|
|
26
|
+
}
|
|
27
|
+
rescue PG::Error => ex
|
|
28
|
+
raise PostgreSQLException.wrap(ex)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
alias update execute
|
|
32
|
+
|
|
33
|
+
def drop_table(name)
|
|
34
|
+
execute "drop table #{name} cascade;"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def drop_table_force(name)
|
|
38
|
+
drop_table name
|
|
39
|
+
rescue PostgreSQLException => err
|
|
40
|
+
@logger.error err.message
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def select(table, &block)
|
|
44
|
+
query = "select * from #{table}"
|
|
45
|
+
@logger.info "[#{@ds.name}] #{query}"
|
|
46
|
+
rs = @connection.exec(query)
|
|
47
|
+
begin
|
|
48
|
+
yield rs
|
|
49
|
+
ensure
|
|
50
|
+
rs.clear
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def vacuum(table)
|
|
55
|
+
execute "vacuum #{table};"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def vacuum_sort_only(table)
|
|
59
|
+
execute "vacuum sort only #{table};"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def analyze(table)
|
|
63
|
+
execute "analyze #{table};"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def log_elapsed_time
|
|
69
|
+
b = Time.now
|
|
70
|
+
return yield
|
|
71
|
+
ensure
|
|
72
|
+
e = Time.now
|
|
73
|
+
t = e - b
|
|
74
|
+
@logger.info "#{'%.1f' % t} secs"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
end
|