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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +4 -0
  3. data/bin/bricolage +6 -0
  4. data/bin/bricolage-jobnet +6 -0
  5. data/jobclass/create.rb +21 -0
  6. data/jobclass/exec.rb +17 -0
  7. data/jobclass/insert-delta.rb +31 -0
  8. data/jobclass/insert.rb +33 -0
  9. data/jobclass/load.rb +39 -0
  10. data/jobclass/my-export.rb +40 -0
  11. data/jobclass/my-migrate.rb +103 -0
  12. data/jobclass/noop.rb +13 -0
  13. data/jobclass/rebuild-drop.rb +37 -0
  14. data/jobclass/rebuild-rename.rb +49 -0
  15. data/jobclass/s3-put.rb +19 -0
  16. data/jobclass/sql.rb +29 -0
  17. data/jobclass/td-delete.rb +20 -0
  18. data/jobclass/td-export.rb +30 -0
  19. data/jobclass/unload.rb +30 -0
  20. data/jobclass/wait-file.rb +48 -0
  21. data/lib/bricolage/application.rb +260 -0
  22. data/lib/bricolage/commandutils.rb +52 -0
  23. data/lib/bricolage/configloader.rb +126 -0
  24. data/lib/bricolage/context.rb +108 -0
  25. data/lib/bricolage/datasource.rb +144 -0
  26. data/lib/bricolage/eventhandlers.rb +47 -0
  27. data/lib/bricolage/exception.rb +47 -0
  28. data/lib/bricolage/filedatasource.rb +42 -0
  29. data/lib/bricolage/filesystem.rb +165 -0
  30. data/lib/bricolage/genericdatasource.rb +37 -0
  31. data/lib/bricolage/job.rb +212 -0
  32. data/lib/bricolage/jobclass.rb +98 -0
  33. data/lib/bricolage/jobfile.rb +100 -0
  34. data/lib/bricolage/jobflow.rb +389 -0
  35. data/lib/bricolage/jobnetrunner.rb +264 -0
  36. data/lib/bricolage/jobresult.rb +74 -0
  37. data/lib/bricolage/logger.rb +52 -0
  38. data/lib/bricolage/mysqldatasource.rb +223 -0
  39. data/lib/bricolage/parameters.rb +653 -0
  40. data/lib/bricolage/postgresconnection.rb +78 -0
  41. data/lib/bricolage/psqldatasource.rb +449 -0
  42. data/lib/bricolage/resource.rb +68 -0
  43. data/lib/bricolage/rubyjobclass.rb +42 -0
  44. data/lib/bricolage/s3datasource.rb +144 -0
  45. data/lib/bricolage/script.rb +120 -0
  46. data/lib/bricolage/sqlstatement.rb +351 -0
  47. data/lib/bricolage/taskqueue.rb +156 -0
  48. data/lib/bricolage/tddatasource.rb +116 -0
  49. data/lib/bricolage/variables.rb +208 -0
  50. data/lib/bricolage/version.rb +4 -0
  51. data/lib/bricolage.rb +8 -0
  52. data/libexec/sqldump +9 -0
  53. data/libexec/sqldump.Darwin +0 -0
  54. data/libexec/sqldump.Linux +0 -0
  55. data/test/all.rb +3 -0
  56. data/test/home/config/development/database.yml +57 -0
  57. data/test/home/config/development/password.yml +2 -0
  58. data/test/home/subsys/separated.job +1 -0
  59. data/test/home/subsys/separated.sql +1 -0
  60. data/test/home/subsys/unified.jobnet +1 -0
  61. data/test/home/subsys/unified.sql.job +5 -0
  62. data/test/test_filesystem.rb +19 -0
  63. data/test/test_parameters.rb +401 -0
  64. data/test/test_variables.rb +114 -0
  65. metadata +192 -0
@@ -0,0 +1,120 @@
1
+ require 'bricolage/resource'
2
+ require 'bricolage/exception'
3
+ require 'forwardable'
4
+ require 'stringio'
5
+
6
+ module Bricolage
7
+
8
+ class Script
9
+ def initialize
10
+ @tasks = []
11
+ @in_task_block = false
12
+ end
13
+
14
+ def task(ds)
15
+ raise FatalError, "nested task is not supported" if @in_task_block
16
+ raise ParameterError, "no data source" unless ds
17
+ @in_task_block = true
18
+ begin
19
+ task = ds.new_task
20
+ yield task
21
+ @tasks.push task
22
+ ensure
23
+ @in_task_block = false
24
+ end
25
+ end
26
+
27
+ def bind(ctx, vars)
28
+ raise "[BUG] unresolved variables given" unless vars.resolved?
29
+ @tasks.each do |task|
30
+ task.bind(ctx, vars)
31
+ end
32
+ end
33
+
34
+ def source
35
+ buf = StringIO.new
36
+ first = true
37
+ @tasks.each do |task|
38
+ buf.puts unless first; first = false
39
+ buf.puts task.source
40
+ end
41
+ buf.string
42
+ end
43
+
44
+ def run
45
+ result = nil
46
+ @tasks.each do |task|
47
+ result = task.run
48
+ end
49
+ result || JobResult.success
50
+ end
51
+
52
+ def run_explain
53
+ @tasks.each do |task|
54
+ if task.respond_to?(:run_explain)
55
+ task.run_explain
56
+ else
57
+ puts "-- task #{task.class} does not support explain; show source"
58
+ puts task.source
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ class DataSourceTask
65
+ def initialize(ds)
66
+ @ds = ds
67
+ @actions = []
68
+ end
69
+
70
+ attr_reader :ds
71
+
72
+ def bind(*args)
73
+ @actions.each do |action|
74
+ action.bind(*args)
75
+ end
76
+ end
77
+
78
+ def run
79
+ result = nil
80
+ @ds.open_for_batch {
81
+ @actions.each do |action|
82
+ result = action.run
83
+ end
84
+ }
85
+ result
86
+ end
87
+
88
+ def source
89
+ buf = StringIO.new
90
+ @actions.each do |action|
91
+ buf.puts action.source
92
+ end
93
+ buf.string
94
+ end
95
+
96
+ private
97
+
98
+ def add(action)
99
+ action.ds = @ds
100
+ @actions.push action
101
+ end
102
+
103
+ def each_action(&block)
104
+ @actions.each(&block)
105
+ end
106
+
107
+ class Action
108
+ extend Forwardable
109
+
110
+ attr_accessor :ds
111
+
112
+ def bind(context, variables)
113
+ end
114
+
115
+ # abstract def run
116
+ # abstract def source
117
+ end
118
+ end
119
+
120
+ end
@@ -0,0 +1,351 @@
1
+ require 'bricolage/filesystem'
2
+ require 'bricolage/exception'
3
+
4
+ module Bricolage
5
+
6
+ class SQLStatement
7
+ def SQLStatement.delete_where(cond, location = nil)
8
+ for_string("delete from $dest_table where #{cond};", location)
9
+ end
10
+
11
+ def SQLStatement.for_string(sql, location = nil)
12
+ new(StringResource.new(sql, location))
13
+ end
14
+
15
+ def initialize(resource, declarations = nil)
16
+ @resource = resource
17
+ @declarations = declarations
18
+ @code = nil
19
+ @replace = nil
20
+ end
21
+
22
+ attr_reader :resource
23
+
24
+ def ==(other)
25
+ return false unless other.kind_of?(SQLStatement)
26
+ @resource == other.resource
27
+ end
28
+
29
+ alias eql? ==
30
+
31
+ def hash
32
+ @resource.hash
33
+ end
34
+
35
+ def declarations
36
+ @declarations ||= Declarations.parse(@resource)
37
+ end
38
+
39
+ attr_writer :declarations
40
+
41
+ def replace(re, subst)
42
+ @replace = [re, subst]
43
+ self
44
+ end
45
+
46
+ def bind(ctx, vars)
47
+ raise FatalError, "already bound SQL statement: #{@resource.name}" if @code
48
+ src = ctx.parameter_file_loader.eruby(@resource.content, @resource.name || '-')
49
+ @code = Variable.expand_string(apply_replace(src)) {|name|
50
+ val = vars[name]
51
+ val.respond_to?(:to_sql) ? val.to_sql : val.to_s
52
+ }
53
+ end
54
+
55
+ def location
56
+ @resource.name
57
+ end
58
+
59
+ def source
60
+ @code or raise FatalError, "unbound SQL statement: #{@resource.name}"
61
+ end
62
+
63
+ def stripped_source
64
+ strip_sql(source)
65
+ end
66
+
67
+ def kind
68
+ # This implementation is not complete but useful
69
+ first_word = @resource.content.gsub(%r<'(?:[^']+|'')*'|"(?:[^"]+|"")*"|--.*|/\*(?m:.*?)\*/>, '').strip.slice(/\A\w+/)
70
+ op = first_word ? first_word.downcase : nil
71
+ case op
72
+ when 'with' then 'select'
73
+ else op
74
+ end
75
+ end
76
+
77
+ def inspect
78
+ "\#<#{self.class} #{@resource.inspect}>"
79
+ end
80
+
81
+ def raw_content
82
+ @resource.content
83
+ end
84
+
85
+ def stripped_raw_content
86
+ strip_sql(raw_content)
87
+ end
88
+
89
+ def meta_data
90
+ SQLMetaDataParser.new.parse(raw_content)
91
+ end
92
+
93
+ def attributes
94
+ Array(meta_data['attributes'])
95
+ end
96
+
97
+ private
98
+
99
+ def apply_replace(content)
100
+ return content unless @replace
101
+ re, subst = @replace
102
+ content.gsub(re, subst)
103
+ end
104
+
105
+ def strip_sql(src)
106
+ src.sub(%r{\A/\*.*?^\*/}m, '').gsub(/^--.*/, '').strip
107
+ end
108
+ end
109
+
110
+ # Variable declarations in parameter files
111
+ class Declarations
112
+ def Declarations.parse(source)
113
+ SQLMetaDataParser.new.parse_declarations(source)
114
+ end
115
+
116
+ def initialize(vars = {})
117
+ @vars = {}
118
+ vars.each do |name, default_value|
119
+ declare name, default_value
120
+ end
121
+ end
122
+
123
+ def inspect
124
+ "\#<#{self.class} #{@vars.inspect}>"
125
+ end
126
+
127
+ def declared?(name)
128
+ @vars.key?(name.to_s)
129
+ end
130
+
131
+ def [](name)
132
+ @vars[name.to_s] or raise ParameterError, "undeclared variable: #{name}"
133
+ end
134
+
135
+ def each(&block)
136
+ @vars.each_value(&block)
137
+ end
138
+
139
+ def default_variables
140
+ Variables.define {|vars|
141
+ each do |decl|
142
+ if decl.have_default_value?
143
+ vars[decl.name.to_s] = decl.default_value
144
+ end
145
+ end
146
+ }
147
+ end
148
+
149
+ def freeze
150
+ @vars.freeze
151
+ super
152
+ end
153
+
154
+ def declare(name, value)
155
+ raise ParameterError, "duplicated variable declaration: #{name}" if declared?(name)
156
+ varname = name.to_s.tr('-', '_')
157
+ @vars[varname] = Declaration.new(varname, value)
158
+ end
159
+
160
+ def declare_table(name, value)
161
+ case value
162
+ when String
163
+ declare name, value
164
+ when nil
165
+ declare name, name
166
+ else
167
+ raise ParameterError, "bad default value for table variable: #{value.class} (#{value.inspect})"
168
+ end
169
+ end
170
+
171
+ def declare_tables(label, value)
172
+ case value
173
+ when String
174
+ declare value, value
175
+ when Array
176
+ value.each do |name|
177
+ declare name, name
178
+ end
179
+ when Hash
180
+ value.each do |name, val|
181
+ declare name, val
182
+ end
183
+ else
184
+ raise ParameterError, "'#{label}' entry value must be a string, a list or a map but a #{value.class}"
185
+ end
186
+ end
187
+
188
+ def declare_map(label, value)
189
+ case value
190
+ when String
191
+ declare value, nil
192
+ when Array
193
+ value.each do |name|
194
+ declare name, nil
195
+ end
196
+ when Hash
197
+ value.each do |name, val|
198
+ declare name, val
199
+ end
200
+ else
201
+ raise ParameterError, "'#{label}' entry value must be a string, a list or a map but a #{value.class}"
202
+ end
203
+ end
204
+
205
+ def declare_none(label, value)
206
+ # ignore
207
+ end
208
+ end
209
+
210
+ # Accepts all variable as declared dynamically
211
+ class DynamicDeclarations
212
+ def declared?(name)
213
+ true
214
+ end
215
+
216
+ def each
217
+ end
218
+
219
+ def [](name)
220
+ Declaration.new(name.to_s, nil)
221
+ end
222
+ end
223
+
224
+ class Declaration
225
+ def initialize(name, default_value)
226
+ @name = name
227
+ @default_value = default_value
228
+ end
229
+
230
+ attr_reader :name
231
+ attr_reader :default_value
232
+
233
+ def have_default_value?
234
+ not @default_value.nil?
235
+ end
236
+
237
+ def inspect
238
+ "\#<#{self.class} #{@name}#{have_default_value? ? '=' + @default_value.inspect : ''}>"
239
+ end
240
+ end
241
+
242
+ class SQLMetaDataParser
243
+ def parse_declarations(source)
244
+ build_decls(parse(source))
245
+ end
246
+
247
+ def parse(source)
248
+ parse_meta(read_mata(source))
249
+ end
250
+
251
+ private
252
+
253
+ DECLARATIONS = {
254
+ 'dest-table' => 'table',
255
+ 'src-tables' => 'tables',
256
+ 'params' => 'map',
257
+ 'attributes' => 'none'
258
+ }
259
+
260
+ def build_decls(map)
261
+ decls = Declarations.new
262
+ DECLARATIONS.each do |key, type|
263
+ value = map.delete(key)
264
+ decls.send "declare_#{type}", key, value if value
265
+ end
266
+ unless map.empty?
267
+ raise ParameterError, "unknown SQL parameter declaration: #{map.keys.join(', ')}"
268
+ end
269
+ decls.freeze
270
+ decls
271
+ end
272
+
273
+ def parse_meta(source)
274
+ return Hash.new if source.empty?
275
+ YAML.load(source) || {}
276
+ rescue YAML::SyntaxError => ex
277
+ raise ParameterError, "SQL meta data syntax error: #{ex.message}"
278
+ end
279
+
280
+ KEYS_RE = /\A--(?:#{DECLARATIONS.keys.join('|')}):/
281
+
282
+ def read_mata(source)
283
+ lines = ''
284
+ source.each_line do |line|
285
+ case line
286
+ when KEYS_RE
287
+ lines.concat line.sub(/\A--/, '')
288
+ when /\A--([\w\-]+):/
289
+ key = $1
290
+ raise ParameterError, "unknown SQL meta data: #{source.name}: #{key}"
291
+ when /\A--/
292
+ # skip non-metadata comments
293
+ lines.concat "\n"
294
+ else
295
+ break
296
+ end
297
+ end
298
+ lines
299
+ end
300
+ end
301
+
302
+ class TableSpec
303
+ # "[SCHEMA.]TABLE" -> TableSpec(SCHEMA, TABLE)
304
+ def TableSpec.parse(spec)
305
+ new(*split_name(spec))
306
+ end
307
+
308
+ # "TABLE" -> [nil, "TABLE"]
309
+ # "SCHEMA.TABLE" -> ["SCHEMA", "TABLE"]
310
+ def TableSpec.split_name(name_pair)
311
+ raise ParameterError, "table spec is empty" if name_pair.strip.empty?
312
+ components = name_pair.split('.', 2)
313
+ if components.size == 1
314
+ return nil, components.first
315
+ else
316
+ s, t = components
317
+ raise ParameterError, "schema name is blank" if not s or s.empty?
318
+ raise ParameterError, "table name is blank" if not t or t.empty?
319
+ return s, t
320
+ end
321
+ end
322
+
323
+ def initialize(schema, name)
324
+ @schema = schema
325
+ @name = name
326
+ end
327
+
328
+ attr_reader :schema
329
+ attr_reader :name
330
+
331
+ def to_s
332
+ @schema ? "#{@schema}.#{@name}" : @name
333
+ end
334
+
335
+ def inspect
336
+ "\#<#{self.class} #{to_s}>"
337
+ end
338
+
339
+ def ==(other)
340
+ return false unless other.kind_of?(TableSpec)
341
+ @schema == other.schema and @name == other.name
342
+ end
343
+
344
+ alias eql? ==
345
+
346
+ def hash
347
+ @schema.hash ^ @name.hash
348
+ end
349
+ end
350
+
351
+ end
@@ -0,0 +1,156 @@
1
+ require 'bricolage/jobflow'
2
+ require 'bricolage/exception'
3
+ require 'pathname'
4
+
5
+ module Bricolage
6
+
7
+ class TaskQueue
8
+ def initialize
9
+ @queue = []
10
+ end
11
+
12
+ def empty?
13
+ @queue.empty?
14
+ end
15
+
16
+ def size
17
+ @queue.size
18
+ end
19
+
20
+ def queued?
21
+ not empty?
22
+ end
23
+
24
+ def each(&block)
25
+ @queue.each(&block)
26
+ end
27
+
28
+ def consume_each
29
+ lock
30
+ save
31
+ while task = self.next
32
+ yield task
33
+ deq
34
+ end
35
+ ensure
36
+ unlock
37
+ end
38
+
39
+ def enq(task)
40
+ @queue.push task
41
+ end
42
+
43
+ def next
44
+ @queue.first
45
+ end
46
+
47
+ def deq
48
+ task = @queue.shift
49
+ save
50
+ task
51
+ end
52
+
53
+ def save
54
+ end
55
+
56
+ def restore
57
+ end
58
+
59
+ def locked?
60
+ false
61
+ end
62
+
63
+ def lock
64
+ end
65
+
66
+ def unlock
67
+ end
68
+
69
+ def unlock_help
70
+ "[MUST NOT HAPPEN] this message must not be shown"
71
+ end
72
+ end
73
+
74
+ class FileTaskQueue < TaskQueue
75
+ def FileTaskQueue.restore_if_exist(path)
76
+ q = new(path)
77
+ q.restore if q.queued?
78
+ q
79
+ end
80
+
81
+ def initialize(path)
82
+ super()
83
+ @path = path
84
+ end
85
+
86
+ def queued?
87
+ @path.exist?
88
+ end
89
+
90
+ def save
91
+ if empty?
92
+ @path.unlink if @path.exist?
93
+ return
94
+ end
95
+ tmpname = "#{@path}.tmp.#{Process.pid}"
96
+ begin
97
+ File.open(tmpname, 'w') {|f|
98
+ each do |task|
99
+ f.puts task.serialize
100
+ end
101
+ }
102
+ File.rename tmpname, @path
103
+ ensure
104
+ FileUtils.rm_f tmpname
105
+ end
106
+ end
107
+
108
+ def restore
109
+ File.foreach(@path) do |line|
110
+ enq JobTask.deserialize(line)
111
+ end
112
+ end
113
+
114
+ def locked?
115
+ lock_file_path.exist?
116
+ end
117
+
118
+ def lock
119
+ FileUtils.touch lock_file_path
120
+ end
121
+
122
+ def unlock
123
+ FileUtils.rm_f lock_file_path
124
+ end
125
+
126
+ def lock_file_path
127
+ Pathname.new("#{@path}.LOCK")
128
+ end
129
+
130
+ def unlock_help
131
+ "remove the file: #{lock_file_path}"
132
+ end
133
+ end
134
+
135
+ class JobTask
136
+ def initialize(jobnet, seq, job)
137
+ @jobnet = jobnet
138
+ @seq = seq
139
+ @job = job
140
+ end
141
+
142
+ attr_reader :jobnet
143
+ attr_reader :seq
144
+ attr_reader :job
145
+
146
+ def serialize
147
+ [@jobnet, @seq, @job].join("\t")
148
+ end
149
+
150
+ def JobTask.deserialize(str)
151
+ jobnet, seq, job = str.strip.split("\t", 3)
152
+ new(JobFlow::Ref.parse(jobnet), seq.to_i, JobFlow::Ref.parse(job))
153
+ end
154
+ end
155
+
156
+ end