bricolage 5.8.7

Sign up to get free protection for your applications and to get access to all the features.
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,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