migr8 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ ###
4
+ ### $Release: 0.4.0 $
5
+ ### $Copyright: copyright(c) 2013 kuwata-lab.com all rights reserved $
6
+ ### $License: MIT License $
7
+ ###
8
+
9
+ require 'migr8'
10
+
11
+ #if __FILE__ == $0
12
+ status = Migr8::Application.main()
13
+ exit(status)
14
+ #end
@@ -0,0 +1,2645 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+
4
+ ###
5
+ ### migr8.py -- DB schema version management tool
6
+ ###
7
+ ### $Release: 0.4.0 $
8
+ ### $Copyright: copyright(c) 2013 kuwata-lab.com all rights reserved $
9
+ ### $License: MIT License $
10
+ ###
11
+
12
+ require 'yaml'
13
+ require 'open3'
14
+ require 'etc'
15
+
16
+
17
+ module Migr8
18
+
19
+ RELEASE = "$Release: 0.4.0 $".split()[1]
20
+
21
+ DEBUG = false
22
+
23
+ def self.DEBUG=(flag)
24
+ remove_const(:DEBUG)
25
+ return const_set(:DEBUG, flag)
26
+ end
27
+
28
+
29
+ class Migr8Error < StandardError
30
+ end
31
+
32
+ class CommandSetupError < Migr8Error
33
+ end
34
+
35
+ class SQLExecutionError < Migr8Error
36
+ end
37
+
38
+ class HistoryFileError < Migr8Error
39
+ end
40
+
41
+ class MigrationFileError < Migr8Error
42
+ end
43
+
44
+ class RepositoryError < Migr8Error
45
+ end
46
+
47
+ class MigrationError < Migr8Error
48
+ end
49
+
50
+
51
+ class Migration
52
+
53
+ attr_accessor :version, :author, :desc, :vars, :up, :down
54
+ attr_accessor :applied_at, :id, :up_script, :down_script
55
+
56
+ def initialize(version=nil, author=nil, desc=nil)
57
+ #; [!y4dy3] takes version, author, and desc arguments.
58
+ @version = version
59
+ @author = author
60
+ @desc = desc
61
+ @vars = {}
62
+ @up = ''
63
+ @down = ''
64
+ end
65
+
66
+ def applied?
67
+ #; [!ebzct] returns false when @applied_at is nil, else true.
68
+ return ! @applied_at.nil?
69
+ end
70
+
71
+ def up_script
72
+ #; [!200k7] returns @up_script if it is set.
73
+ return @up_script if @up_script
74
+ #; [!cfp34] returns nil when 'up' is not set.
75
+ return @up unless @up
76
+ #; [!6gaxb] returns 'up' string expanding vars in it.
77
+ #; [!jeomg] renders 'up' script as eRuby template.
78
+ return _render(Util::Expander.expand_str(@up, @vars))
79
+ end
80
+
81
+ def down_script
82
+ #; [!27n2l] returns @down_script if it is set.
83
+ return @down_script if @down_script
84
+ #; [!e45s1] returns nil when 'down' is not set.
85
+ return @down unless @down
86
+ #; [!0q3nq] returns 'down' string expanding vars in it.
87
+ #; [!kpwut] renders 'up' script as eRuby template.
88
+ return _render(Util::Expander.expand_str(@down, @vars))
89
+ end
90
+
91
+ def _render(str)
92
+ #require 'erb'
93
+ #return ERB.new(str, nil, '<>').result(binding())
94
+ #; [!1w3ov] renders string with 'vars' as context variables.
95
+ return Util::Template.new(str).render(@vars)
96
+ end
97
+ private :_render
98
+
99
+ def applied_at_or(default)
100
+ #; [!zazux] returns default arugment when not applied.
101
+ return default unless applied?
102
+ #; [!fxb4y] returns @applied_at without msec.
103
+ return @applied_at.split(/\./)[0] # '12:34:56.789' -> '12:34:56'
104
+ end
105
+
106
+ def filepath
107
+ #; [!l9t5k] returns nil when version is not set.
108
+ return nil unless @version
109
+ #; [!p0d9q] returns filepath of migration file.
110
+ return Repository.new(nil).migration_filepath(@version)
111
+ end
112
+
113
+ def self.load_from(filepath)
114
+ #; [!fbea5] loads data from file and returns migration object.
115
+ data = File.open(filepath) {|f| YAML.load(f) }
116
+ mig = self.new(data['version'], data['author'], data['desc'])
117
+ #; [!sv21s] expands values of 'vars'.
118
+ mig.vars = Util::Expander.expand_vars(data['vars'])
119
+ #; [!32ns3] not expand both 'up' and 'down'.
120
+ mig.up = data['up']
121
+ mig.down = data['down']
122
+ return mig
123
+ end
124
+
125
+ end
126
+
127
+
128
+ class Repository
129
+
130
+ HISTORY_FILEPATH = 'migr8/history.txt'
131
+ HISTORY_TABLE = '_migr8_history'
132
+ MIGRATION_DIRPATH = 'migr8/migrations/'
133
+
134
+ attr_reader :dbms
135
+
136
+ def initialize(dbms=nil)
137
+ @dbms = dbms
138
+ end
139
+
140
+ def history_filepath()
141
+ return HISTORY_FILEPATH
142
+ end
143
+
144
+ def migration_filepath(version)
145
+ return "#{MIGRATION_DIRPATH}#{version}.yaml"
146
+ end
147
+
148
+ def parse_history_file()
149
+ fpath = history_filepath()
150
+ tuples = []
151
+ eol = nil
152
+ File.open(fpath) do |f|
153
+ i = 0
154
+ f.each do |line|
155
+ i += 1
156
+ eol = line[-1]
157
+ line.strip!
158
+ next if line =~ /\A\#/
159
+ next if line.empty?
160
+ line =~ /\A([-\w]+)[ \t]*\# \[(.*)\][ \t]*(.*)\z/ or
161
+ raise HistoryFileError.new("File '#{fpath}', line #{i}: invalid format.\n #{line}")
162
+ version, author, desc = $1, $2, $3
163
+ tuples << [version, author, desc]
164
+ end
165
+ end
166
+ eol == ?\n or
167
+ raise HistoryFileError.new("missing newline character (\"\\n\") at end of history file.
168
+ Plese open it by `migr8.rb hist -o` and add newline character at end of file.")
169
+ return tuples
170
+ end
171
+
172
+ def rebuild_history_file()
173
+ tuples = parse_history_file()
174
+ s = "# -*- coding: utf-8 -*-\n"
175
+ tuples.each do |version, author, desc|
176
+ s << _to_line(version, author, desc)
177
+ end
178
+ fpath = history_filepath()
179
+ File.open(fpath, 'w') {|f| f.write(s) }
180
+ return s
181
+ end
182
+
183
+ def migrations_in_history_file(applied_migrations_dict=nil)
184
+ dict = applied_migrations_dict # {version=>applied_at}
185
+ applied = nil
186
+ tuples = parse_history_file()
187
+ fpath = history_filepath()
188
+ migrations = tuples.collect {|version, author, desc|
189
+ mig = load_migration(version) or
190
+ raise HistoryFileError.new("#{version}: migration file not found (please edit history file by 'migr8.rb hist -o' and delete or comment out it).")
191
+ mig.version == version or
192
+ raise MigrationError.new("#{version}: version in migration file (='mig.filepath') should be '#{version}' but got #{mig.version}.
193
+ Please run '#{File.basename($0)} edit #{version}' and fix version in that file.")
194
+ #$stderr << "# WARNING: #{version}: version in history file is not match to #{fpath}\n"
195
+ mig.author == author or
196
+ $stderr << "# WARNING: #{version}: author in history file is not match to #{fpath}\n"
197
+ mig.desc == desc or
198
+ $stderr << "# WARNING: #{version}: description in history file is not match to #{fpath}\n"
199
+ mig.applied_at = applied.applied_at if dict && (applied = dict.delete(mig.version))
200
+ mig
201
+ }
202
+ return migrations
203
+ end
204
+
205
+ def migrations_in_history_table()
206
+ return @dbms.get_migrations()
207
+ end
208
+
209
+ def load_migration(version)
210
+ fpath = migration_filepath(version)
211
+ return nil unless File.file?(fpath)
212
+ return Migration.load_from(fpath)
213
+ end
214
+
215
+ def apply_migrations(migs)
216
+ @dbms.apply_migrations(migs)
217
+ end
218
+
219
+ def unapply_migrations(migs, down_script_in_db=false)
220
+ @dbms.unapply_migrations(migs, down_script_in_db)
221
+ end
222
+
223
+ def fetch_details_from_history_table(mig)
224
+ s = @dbms.fetch_column_value_of(mig.version, 'applied_at')
225
+ s = s.strip if s
226
+ mig.applied_at = (s.nil? || s.empty? ? nil : s)
227
+ mig.up_script = @dbms.fetch_column_value_of(mig.version, 'up_script')
228
+ mig.down_script = @dbms.fetch_column_value_of(mig.version, 'down_script')
229
+ end
230
+
231
+ def new_version
232
+ while true
233
+ version = _new_version()
234
+ break unless File.file?(migration_filepath(version))
235
+ end
236
+ return version
237
+ end
238
+
239
+ def _new_version
240
+ version = ''
241
+ s = VERSION_CHARS
242
+ n = s.length - 1
243
+ 4.times { version << s[rand(n)] }
244
+ d = VERSION_DIGITS
245
+ n = d.length - 1
246
+ 4.times { version << d[rand(n)] }
247
+ return version
248
+ end
249
+
250
+ VERSION_CHARS = ('a'..'z').to_a - ['l']
251
+ VERSION_DIGITS = ('0'..'9').to_a - ['1']
252
+
253
+ def init()
254
+ verbose = true
255
+ ## create directory
256
+ path = migration_filepath('_dummy_')
257
+ dirs = []
258
+ while ! (path = File.dirname(path)).empty? && path != '.' && path != '/'
259
+ dirs << path
260
+ end
261
+ dirs.reverse_each do |dir|
262
+ if ! File.directory?(dir)
263
+ puts "$ mkdir #{dir}" if verbose
264
+ Dir.mkdir(dir)
265
+ end
266
+ end
267
+ ## create history file
268
+ fpath = history_filepath()
269
+ if ! File.file?(fpath)
270
+ magic = '# -*- coding: utf-8 -*-'
271
+ puts "$ echo '#{magic}' > #{fpath}" if verbose
272
+ File.open(fpath, 'w') {|f| f.write(magic+"\n") }
273
+ end
274
+ ## create history table
275
+ @dbms.create_history_table()
276
+ end
277
+
278
+ def init?
279
+ return false unless File.file?(history_filepath())
280
+ return false unless File.directory?(File.dirname(migration_filepath('_')))
281
+ return false unless @dbms.history_table_exist?
282
+ return true
283
+ end
284
+
285
+ def history_file_exist?
286
+ fpath = history_filepath()
287
+ return File.file?(fpath)
288
+ end
289
+
290
+ def history_file_empty?
291
+ fpath = history_filepath()
292
+ return true unless File.file?(fpath)
293
+ exist_p = File.open(fpath, 'rb') {|f|
294
+ f.any? {|line| line =~ /\A\s*\w+/ }
295
+ }
296
+ return ! exist_p
297
+ end
298
+
299
+ def migration_file_exist?(version)
300
+ return File.exist?(migration_filepath(version))
301
+ end
302
+
303
+ def create_migration(version=nil, author=nil, desc="", opts={})
304
+ if version && migration_file_exist?(version)
305
+ raise MigrationError.new("#{version}: migration file already exists.")
306
+ end
307
+ mig = Migration.new(version || new_version(), author || Etc.getlogin(), desc)
308
+ content = render_migration_file(mig, opts)
309
+ File.open(mig.filepath, 'wb') {|f| f.write(content) }
310
+ File.open(history_filepath(), 'ab') {|f| f.write(to_line(mig)) }
311
+ return mig
312
+ end
313
+
314
+ def delete_migration(version)
315
+ mig = load_migration(version) or
316
+ raise MigrationError.new("#{version}: migration not found.")
317
+ fetch_details_from_history_table(mig)
318
+ ! mig.applied? or
319
+ raise MigrationError.new("#{version}: already applied.
320
+ Please run `#{File.basename($0)} unapply #{version}` at first if you want to delete it.")
321
+ #
322
+ File.open(history_filepath(), 'r+') do |f|
323
+ content = f.read()
324
+ content.gsub!(/^#{version}\b.*\n/, '')
325
+ f.rewind()
326
+ f.truncate(0)
327
+ f.write(content)
328
+ end
329
+ File.unlink(migration_filepath(version))
330
+ return mig
331
+ end
332
+
333
+ protected
334
+
335
+ def to_line(mig) # :nodoc:
336
+ return _to_line(mig.version, mig.author, mig.desc)
337
+ end
338
+
339
+ def _to_line(version, author, desc)
340
+ return "%-10s # [%s] %s\n" % [version, author, desc]
341
+ end
342
+
343
+ def render_migration_file(mig, opts={}) # :nodoc:
344
+ return @dbms.new_skeleton().render(mig, opts)
345
+ end
346
+
347
+ end
348
+
349
+
350
+ class RepositoryOperation
351
+
352
+ def initialize(repo)
353
+ @repo = repo
354
+ end
355
+
356
+ def history
357
+ mig_hist, mig_dict = _get_migrations_hist_and_applied()
358
+ s = ""
359
+ mig_hist.each do |mig|
360
+ s << _to_line(mig)
361
+ end
362
+ if ! mig_dict.empty?
363
+ puts "## Applied to DB but not exist in history file:"
364
+ mig_dict.each {|mig| s << _to_line(mig) }
365
+ end
366
+ return s
367
+ end
368
+
369
+ def new(version, author, desc, opts={})
370
+ if version && @repo.migration_file_exist?(version)
371
+ raise MigrationError.new("#{version}: failed to create migration file because file already exists.
372
+ Please run 'File.basename($0) edit #{version}' to see existing file.")
373
+ end
374
+ mig = @repo.create_migration(version, author, desc, opts)
375
+ return mig
376
+ end
377
+
378
+ def inspect(n=5)
379
+ mig_hist, mig_dict = _get_migrations_hist_and_applied()
380
+ pos = mig_hist.length - n - 1
381
+ i = mig_hist.index {|mig| ! mig.applied? } # index of oldest unapplied
382
+ j = mig_hist.rindex {|mig| mig.applied? } # index of newest applied
383
+ start = i.nil? ? pos : [i - 1, pos].min
384
+ start = 0 if start < 0
385
+ if mig_hist.empty?
386
+ status = "no migrations"
387
+ recent = nil
388
+ elsif i.nil?
389
+ status = "all applied"
390
+ recent = mig_hist[start..-1]
391
+ elsif j.nil?
392
+ status = "nothing applied"
393
+ recent = mig_hist[0..-1]
394
+ elsif i < j
395
+ status = "YOU MUST APPLY #{mig_hist[i].version} AT FIRST!"
396
+ recent = mig_hist[start..-1]
397
+ else
398
+ count = mig_hist.length - i
399
+ status = "there are #{count} migrations to apply"
400
+ status = "there is a migration to apply" if count == 1
401
+ recent = mig_hist[start..-1]
402
+ end
403
+ missing = mig_dict.empty? ? nil : mig_dict.values
404
+ return {:status=>status, :recent=>recent, :missing=>missing}
405
+ end
406
+
407
+ def status
408
+ ret = inspect()
409
+ s = ""
410
+ s << "## Status: #{ret[:status]}\n"
411
+ if ret[:recent]
412
+ s << "## Recent history:\n"
413
+ ret[:recent].each {|mig| s << _to_line(mig) }
414
+ end
415
+ if ret[:missing]
416
+ s << "## !!! The following migrations are applied to DB, but files are not found.\n"
417
+ s << "## !!! (Try `#{File.basename($0)} unapply -x abcd1234` to unapply them.)\n"
418
+ ret[:missing].each {|mig| s << _to_line(mig) }
419
+ end
420
+ return s
421
+ end
422
+
423
+ def show(version=nil, load_from_db=False)
424
+ migs = load_from_db ? @repo.migrations_in_history_table() \
425
+ : @repo.migrations_in_history_file()
426
+ if version
427
+ mig = migs.find {|mig| mig.version == version } or
428
+ raise MigrationError.new("#{version}: no such migration.")
429
+ else
430
+ mig = migs.last or
431
+ raise MigrationError.new("no migrations to show.")
432
+ end
433
+ if load_from_db
434
+ @repo.fetch_details_from_history_table(mig)
435
+ #assert mig.instance_variable_get('@up_script') != nil
436
+ #assert mig.instance_variable_get('@down_script') != nil
437
+ end
438
+ #
439
+ buf = ""
440
+ buf << "version: #{mig.version}\n"
441
+ buf << "desc: #{mig.desc}\n"
442
+ buf << "author: #{mig.author}\n"
443
+ buf << "vars:\n" unless load_from_db
444
+ mig.vars.each do |k, v|
445
+ buf << " - %-10s " % ["#{k}:"] << v.inspect << "\n"
446
+ end unless load_from_db
447
+ buf << "applied_at: #{mig.applied_at}\n" if load_from_db
448
+ buf << "\n"
449
+ buf << "up: |\n"
450
+ buf << mig.up_script.gsub(/^/, ' ')
451
+ buf << "\n"
452
+ buf << "down: |\n"
453
+ buf << mig.down_script.gsub(/^/, ' ')
454
+ buf << "\n"
455
+ return buf
456
+ end
457
+
458
+ def delete(version)
459
+ @repo.delete_migration(version)
460
+ end
461
+
462
+ def upgrade(n)
463
+ migs_hist, migs_dict = _get_migrations_hist_and_applied()
464
+ ## index of current version
465
+ curr = migs_hist.rindex {|mig| mig.applied? }
466
+ ## error when unapplied older version exists
467
+ if curr
468
+ j = migs_hist.index {|mig| ! mig.applied? }
469
+ raise MigrationError.new("apply #{migs_hist[j].version} at first.") if j && j < curr
470
+ end
471
+ ## unapplied migrations
472
+ migs_unapplied = curr ? migs_hist[(curr+1)..-1] : migs_hist
473
+ ## apply n migrations
474
+ migs_to_apply = n.nil? ? migs_unapplied : migs_unapplied[0...n]
475
+ if migs_to_apply.empty?
476
+ puts "## (nothing to apply)"
477
+ else
478
+ #migs_to_apply.each do |mig|
479
+ # puts "## applying #{mig.version} \# [#{mig.author}] #{mig.desc}"
480
+ # @repo.apply_migration(mig)
481
+ #end
482
+ @repo.apply_migrations(migs_to_apply)
483
+ end
484
+ end
485
+
486
+ def downgrade(n)
487
+ migs_hist, migs_dict = _get_migrations_hist_and_applied()
488
+ ## index of current version
489
+ curr = migs_hist.rindex {|mig| mig.applied? }
490
+ ## error when unapplied older version exists in target migrations
491
+ migs_applied = curr ? migs_hist[0..curr] : []
492
+ if curr
493
+ j = migs_applied.index {|mig| ! mig.applied? }
494
+ raise MigrationError.new("apply #{migs_applied[j].version} at first.") if j && j < curr
495
+ end
496
+ ## unapply n migrations
497
+ migs_to_unapply = n && n < migs_applied.length ? migs_applied[-n..-1] \
498
+ : migs_applied
499
+ if migs_to_unapply.empty?
500
+ puts "## (nothing to unapply)"
501
+ else
502
+ #migs_to_unapply.reverse_each do |mig|
503
+ # puts "## unapplying #{mig.version} \# [#{mig.author}] #{mig.desc}"
504
+ # @repo.unapply_migration(mig)
505
+ #end
506
+ @repo.unapply_migrations(migs_to_unapply.reverse())
507
+ end
508
+ end
509
+
510
+ def apply(versions)
511
+ migs = _get_migrations_in_history_file(versions, false)
512
+ @repo.apply_migrations(migs)
513
+ end
514
+
515
+ def unapply(versions)
516
+ migs = _get_migrations_in_history_file(versions, true)
517
+ @repo.unapply_migrations(migs)
518
+ end
519
+
520
+ def unapply_only_in_database(versions)
521
+ migs = _get_migrations_only_in_database(versions)
522
+ @repo.unapply_migrations(migs, true)
523
+ end
524
+
525
+ private
526
+
527
+ def _to_line(mig, str='(not applied) ')
528
+ return "#{mig.version} #{mig.applied_at_or(str)} \# [#{mig.author}] #{mig.desc}\n"
529
+ end
530
+
531
+ def _get_migrations_hist_and_applied
532
+ ## applied migrations
533
+ mig_applied = {} # {version=>migration}
534
+ @repo.migrations_in_history_table().each {|mig| mig_applied[mig.version] = mig }
535
+ ## migrations in history file
536
+ mig_hist = @repo.migrations_in_history_file()
537
+ mig_hist.each do |migration|
538
+ mig = mig_applied.delete(migration.version)
539
+ migration.applied_at = mig.applied_at if mig
540
+ end
541
+ ##
542
+ return mig_hist, mig_applied
543
+ end
544
+
545
+ def _get_migrations_in_history_file(versions, should_applied)
546
+ mig_hist, _ = _get_migrations_hist_and_applied()
547
+ mig_dict = {}
548
+ mig_hist.each {|mig| mig_dict[mig.version] = mig }
549
+ ver_cnt = {}
550
+ migrations = versions.collect {|ver|
551
+ ver_cnt[ver].nil? or
552
+ raise MigrationError.new("#{ver}: specified two or more times.")
553
+ ver_cnt[ver] = 1
554
+ @repo.load_migration(ver) or
555
+ raise MigrationError.new("#{ver}: migration file not found.")
556
+ mig = mig_dict[ver] or
557
+ raise MigrationError.new("#{ver}: no such version in history file.")
558
+ if should_applied
559
+ mig.applied_at or
560
+ raise MigrationError.new("#{ver}: not applied yet.")
561
+ else
562
+ ! mig.applied_at or
563
+ raise MigrationError.new("#{ver}: already applied.")
564
+ end
565
+ mig
566
+ }
567
+ return migrations
568
+ end
569
+
570
+ def _get_migrations_only_in_database(versions)
571
+ mig_hist, mig_applied_dict = _get_migrations_hist_and_applied()
572
+ mig_hist_dict = {}
573
+ mig_hist.each {|mig| mig_hist_dict[mig.version] = mig }
574
+ ver_cnt = {}
575
+ migrations = versions.collect {|ver|
576
+ ver_cnt[ver].nil? or
577
+ raise MigrationError.new("#{ver}: specified two or more times.")
578
+ ver_cnt[ver] = 1
579
+ mig_hist_dict[ver].nil? or
580
+ raise MigrationError.new("#{ver}: version exists in history file (please specify versions only in database).")
581
+ mig = mig_applied_dict[ver] or
582
+ raise MigrationError.new("#{ver}: no such version in database.")
583
+ mig
584
+ }
585
+ migrations.sort_by! {|mig| - mig.id } # sort by reverse order
586
+ return migrations
587
+ end
588
+
589
+ end
590
+
591
+
592
+ class BaseSkeleton
593
+
594
+ def render(mig, opts={})
595
+ plain = opts[:plain]
596
+ buf = ""
597
+ buf << "# -*- coding: utf-8 -*-\n"
598
+ buf << "\n"
599
+ buf << "version: #{mig.version}\n"
600
+ buf << "desc: #{mig.desc}\n"
601
+ buf << "author: #{mig.author}\n"
602
+ buf << "vars:\n"
603
+ buf << _section_vars(mig, opts) unless plain
604
+ buf << "\n"
605
+ buf << "up: |\n"
606
+ buf << _section_up(mig, opts) unless plain
607
+ buf << "\n"
608
+ buf << "down: |\n"
609
+ buf << _section_down(mig, opts) unless plain
610
+ buf << "\n"
611
+ return buf
612
+ end
613
+
614
+ protected
615
+
616
+ def _section_vars(mig, opts)
617
+ tblcol_rexp = /\A(\w+)(?:\.(\w+)|\((\w+)\))\z/
618
+ if (val = opts[:table])
619
+ val =~ /\A(\w+)\z/; table = $1
620
+ return " - table: #{table}\n"
621
+ elsif (val = opts[:column])
622
+ val =~ tblcol_rexp; table = $1; column = $2||$3
623
+ return " - table: #{table}\n" +
624
+ " - column: #{column}\n"
625
+ elsif (val = opts[:index])
626
+ val =~ tblcol_rexp; table = $1; column = $2||$3
627
+ return " - table: #{table}\n" +
628
+ " - column: #{column}\n" +
629
+ " - index: ${table}_${column}_idx\n"
630
+ elsif (val = opts[:unique])
631
+ val =~ tblcol_rexp; table = $1; column = $2||$3
632
+ return " - table: #{table}\n" +
633
+ " - column: #{column}\n" +
634
+ " - unique: ${table}_${column}_unq\n"
635
+ else
636
+ return <<END
637
+ - table: table123
638
+ - column: column123
639
+ - index: ${table}_${column}_idx
640
+ - unique: ${table}_${column}_unq
641
+ END
642
+ end
643
+ end
644
+
645
+ def _section_up(mig, opts)
646
+ return ""
647
+ end
648
+
649
+ def _section_down(mig, opts)
650
+ return ""
651
+ end
652
+
653
+ end
654
+
655
+
656
+ module DBMS
657
+
658
+ def self.detect_by_command(command)
659
+ return Base.detect_by_command(command)
660
+ end
661
+
662
+
663
+ class Base
664
+
665
+ attr_reader :command
666
+ attr_accessor :history_table
667
+ attr_accessor :sqltmpfile # :nodoc:
668
+
669
+ def initialize(command=nil)
670
+ @command = command
671
+ @history_table = Repository::HISTORY_TABLE
672
+ @sqltmpfile = 'migr8/tmp.sql'
673
+ end
674
+
675
+ def execute_sql(sql, cmdopt=nil)
676
+ output, error = Open3.popen3("#{@command} #{cmdopt}") do |sin, sout, serr|
677
+ sin.write(sql)
678
+ sin.close() # important!
679
+ [sout.read(), serr.read()]
680
+ end
681
+ #if output && ! output.empty?
682
+ # $stdout << output
683
+ #end
684
+ if error && ! error.empty?
685
+ $stderr << error
686
+ raise SQLExecutionError.new
687
+ end
688
+ return output
689
+ end
690
+
691
+ def run_sql(sql, opts={})
692
+ verbose = opts[:verbose]
693
+ tmpfile = sqltmpfile()
694
+ puts "$ cat <<_END_ > #{tmpfile}" if verbose
695
+ puts sql if verbose
696
+ puts "_END_" if verbose
697
+ File.open(tmpfile, 'w') {|f| f.write(sql) }
698
+ puts "$ #{@command} < #{tmpfile}" if verbose
699
+ ok = system("#{@command} < #{tmpfile}")
700
+ ok or
701
+ raise SQLExecutionError.new("Failed to run sql ('#{tmpfile}').")
702
+ File.unlink(tmpfile) unless Migr8::DEBUG
703
+ end
704
+
705
+ def create_history_table()
706
+ return false if history_table_exist?
707
+ sql = _history_table_statement()
708
+ run_sql(sql, :verbose=>true)
709
+ return true
710
+ end
711
+
712
+ def _history_table_statement()
713
+ return <<END
714
+ CREATE TABLE #{history_table()} (
715
+ id INTEGER PRIMARY KEY,
716
+ version VARCHAR(40) NOT NULL UNIQUE,
717
+ author VARCHAR(40) NOT NULL,
718
+ description VARCHAR(255) NOT NULL,
719
+ up_script TEXT NOT NULL,
720
+ down_script TEXT NOT NULL,
721
+ applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
722
+ );
723
+ END
724
+ end
725
+ protected :_history_table_statement
726
+
727
+ def history_table_exist?
728
+ raise NotImplementedError.new("#{self.class.name}#history_table_exist?: not implemented yet.")
729
+ end
730
+
731
+ def get_migrations()
732
+ cmdopt = ""
733
+ separator = "|"
734
+ return _get_girations(cmdopt, separator)
735
+ end
736
+
737
+ protected
738
+
739
+ def _get_migrations(cmdopt, separator)
740
+ sql = "SELECT id, version, applied_at, author, description FROM #{history_table()} ORDER BY id;"
741
+ output = execute_sql(sql, cmdopt)
742
+ migs = []
743
+ output.each_line do |line|
744
+ line.strip!
745
+ break if line.empty?
746
+ id, version, applied_at, author, desc = line.strip.split(separator, 5)
747
+ mig = Migration.new(version.strip, author.strip, desc.strip)
748
+ mig.id = Integer(id)
749
+ mig.applied_at = applied_at ? applied_at.split(/\./)[0] : nil
750
+ migs << mig
751
+ end
752
+ return migs
753
+ end
754
+
755
+ def fetch_column_value_of(version, column)
756
+ sql = "SELECT #{column} FROM #{history_table()} WHERE version = '#{version}';"
757
+ down_script = _execute_sql_and_get_column_as_text(sql)
758
+ return down_script
759
+ end
760
+ public :fetch_column_value_of
761
+
762
+ def _execute_sql_and_get_column_as_text(sql)
763
+ cmdopt = ""
764
+ return execute_sql(sql, cmdopt)
765
+ end
766
+
767
+ def _echo_message(msg)
768
+ raise NotImplementedError.new("#{self.class.name}#_echo_message(): not implemented yet.")
769
+ end
770
+
771
+ def _applying_sql(mig)
772
+ msg = "## applying #{mig.version} \# [#{mig.author}] #{mig.desc}"
773
+ sql = <<END
774
+ ---------------------------------------- applying #{mig.version} ----------
775
+ #{_echo_message(msg)}
776
+ -----
777
+ #{mig.up_script};
778
+ -----
779
+ INSERT INTO #{@history_table} (version, author, description, up_script, down_script)
780
+ VALUES ('#{q(mig.version)}', '#{q(mig.author)}', '#{q(mig.desc)}', '#{q(mig.up_script)}', '#{q(mig.down_script)}');
781
+ END
782
+ return sql
783
+ end
784
+
785
+ def _unapplying_sql(mig)
786
+ msg = "## unapplying #{mig.version} \# [#{mig.author}] #{mig.desc}"
787
+ sql = <<END
788
+ ---------------------------------------- unapplying #{mig.version} ----------
789
+ #{_echo_message(msg)}
790
+ -----
791
+ #{mig.down_script};
792
+ -----
793
+ DELETE FROM #{@history_table} WHERE VERSION = '#{mig.version}';
794
+ END
795
+ return sql
796
+ end
797
+
798
+ public
799
+
800
+ def apply_migrations(migs)
801
+ _do_migrations(migs) {|mig| _applying_sql(mig) }
802
+ end
803
+
804
+ def unapply_migrations(migs, down_script_in_db=false)
805
+ if down_script_in_db
806
+ migs.each do |mig|
807
+ mig.down_script = fetch_column_value_of(mig.version, 'down_script')
808
+ end
809
+ end
810
+ _do_migrations(migs) {|mig| _unapplying_sql(mig) }
811
+ end
812
+
813
+ protected
814
+
815
+ def _do_migrations(migs)
816
+ sql = ""
817
+ sql << "BEGIN; /** start transaction **/\n\n"
818
+ sql << migs.collect {|mig| yield mig }.join("\n")
819
+ sql << "\nCOMMIT; /** end transaction **/\n"
820
+ run_sql(sql)
821
+ end
822
+
823
+ public
824
+
825
+ def q(str)
826
+ return str.gsub(/\'/, "''")
827
+ end
828
+
829
+ def new_skeleton()
830
+ return self.class.const_get(:Skeleton).new
831
+ end
832
+
833
+ ##
834
+
835
+ @subclasses = []
836
+
837
+ def self.inherited(klass)
838
+ @subclasses << klass
839
+ end
840
+
841
+ def self.detect_by_command(command)
842
+ klazz = @subclasses.find {|klass| command =~ klass.const_get(:PATTERN) }
843
+ return klazz ? klazz.new(command) : nil
844
+ end
845
+
846
+ end
847
+
848
+
849
+ class SQLite3 < Base
850
+ SYMBOL = 'sqlite3'
851
+ PATTERN = /\bsqlite3\b/
852
+
853
+ def execute_sql(sql, cmdopt=nil)
854
+ preamble = ".bail ON\n"
855
+ return super(preamble+sql, cmdopt)
856
+ end
857
+
858
+ def run_sql(sql, opts={})
859
+ preamble = ".bail ON\n"
860
+ super(preamble+sql, opts)
861
+ end
862
+
863
+ protected
864
+
865
+ def _histrory_table_statement()
866
+ sql = super
867
+ sql = sql.sub(/PRIMARY KEY/, 'PRIMARY KEY AUTOINCREMENT')
868
+ return sql
869
+ end
870
+
871
+ def _execute_sql_and_get_column_as_text(sql)
872
+ cmdopt = "-list"
873
+ s = execute_sql(sql, cmdopt)
874
+ s.sub!(/\r?\n\z/, '') # remove "\n" at end
875
+ return s
876
+ end
877
+
878
+ def _echo_message(msg)
879
+ return ".print '#{q(msg)}'"
880
+ end
881
+
882
+ public
883
+
884
+ def history_table_exist?
885
+ table = history_table()
886
+ output = execute_sql(".table #{table}")
887
+ return output.include?(table)
888
+ end
889
+
890
+ def get_migrations()
891
+ migrations = _get_migrations("-list", /\|/)
892
+ return migrations
893
+ end
894
+
895
+ class Skeleton < BaseSkeleton
896
+
897
+ protected
898
+
899
+ def _section_vars(mig, opts)
900
+ super
901
+ end
902
+
903
+ def _section_up(mig, opts)
904
+ return <<END if opts[:table]
905
+ create table ${table} (
906
+ id integer primary key autoincrement,
907
+ version integer not null default 0,
908
+ name text not null,
909
+ created_at timestamp not null default current_timestamp,
910
+ updated_at timestamp,
911
+ deleted_at timestamp
912
+ );
913
+ create index ${table}_name_idx on ${table}(name);
914
+ create index ${table}_created_at_idx on ${table}(created_at);
915
+ END
916
+ return <<END if opts[:column]
917
+ alter table ${table} add column ${column} integer not null default 0;
918
+ END
919
+ return <<END if opts[:index]
920
+ create index ${index} on ${table}(${column});
921
+ END
922
+ return <<END if opts[:unique]
923
+ create unique index ${unique} on ${table}(${column});
924
+ END
925
+ return <<END
926
+ ---
927
+ --- create table or index
928
+ ---
929
+ create table ${table} (
930
+ id integer primary key autoincrement,
931
+ version integer not null default 0,
932
+ name text not null unique,
933
+ created_at timestamp not null default current_timestamp,
934
+ updated_at timestamp,
935
+ deleted_at timestamp
936
+ );
937
+ create index ${index} on ${table}(${column});
938
+ ---
939
+ --- add column
940
+ ---
941
+ alter table ${table} add column ${column} string not null default '';
942
+ END
943
+ end
944
+
945
+ def _section_down(mig, opts)
946
+ return <<END if opts[:table]
947
+ drop table ${table};
948
+ END
949
+ return <<END if opts[:column]
950
+ alter table ${table} drop column ${column};
951
+ END
952
+ return <<END if opts[:index]
953
+ drop index ${index};
954
+ END
955
+ return <<END if opts[:unique]
956
+ drop index ${unique};
957
+ END
958
+ return <<END
959
+ ---
960
+ --- drop table or index
961
+ ---
962
+ drop table ${table};
963
+ drop index ${index};
964
+ END
965
+ end
966
+
967
+ end
968
+
969
+ end
970
+
971
+
972
+ class PostgreSQL < Base
973
+ SYMBOL = 'postgres'
974
+ PATTERN = /\bpsql\b/
975
+
976
+ def execute_sql(sql, cmdopt=nil)
977
+ preamble = "SET client_min_messages TO WARNING;\n"
978
+ return super(preamble+sql, cmdopt)
979
+ end
980
+
981
+ def run_sql(sql, opts={})
982
+ preamble = "SET client_min_messages TO WARNING;\n"+
983
+ "\\set ON_ERROR_STOP ON\n"
984
+ super(preamble+sql, opts)
985
+ end
986
+
987
+ protected
988
+
989
+ def _history_table_statement()
990
+ sql = super
991
+ sql = sql.sub(/INTEGER/, 'SERIAL ')
992
+ sql = sql.sub('CURRENT_TIMESTAMP', 'TIMEOFDAY()::TIMESTAMP')
993
+ return sql
994
+ end
995
+
996
+ def _execute_sql_and_get_column_as_text(sql)
997
+ cmdopt = "-t -A"
998
+ s = execute_sql(sql, cmdopt)
999
+ s.sub!(/\r?\n\z/, '') # remove "\n" at end
1000
+ return s
1001
+ end
1002
+
1003
+ def _echo_message(msg)
1004
+ return "\\echo '#{q(msg)}'"
1005
+ end
1006
+
1007
+ public
1008
+
1009
+ def history_table_exist?
1010
+ table = history_table()
1011
+ output = execute_sql("\\dt #{table}")
1012
+ return output.include?(table)
1013
+ end
1014
+
1015
+ def get_migrations()
1016
+ migrations = _get_migrations("-qt", / \| /)
1017
+ return migrations
1018
+ end
1019
+
1020
+ class Skeleton < BaseSkeleton
1021
+
1022
+ protected
1023
+
1024
+ def _section_vars(mig, opts)
1025
+ super
1026
+ end
1027
+
1028
+ def _section_up(mig, opts)
1029
+ return <<END if opts[:table]
1030
+ create table ${table} (
1031
+ id serial primary key,
1032
+ version integer not null default 0,
1033
+ name varchar(255) not null,
1034
+ created_at timestamp not null default current_timestamp,
1035
+ updated_at timestamp,
1036
+ deleted_at timestamp
1037
+ );
1038
+ create index ${table}_name_idx on ${table}(name);
1039
+ create index ${table}_created_at_idx on ${table}(created_at);
1040
+ create unique index ${table}_col1_col2_col3_unq on ${table}(col1, col2, col3);
1041
+ END
1042
+ return <<END if opts[:column]
1043
+ alter table ${table} add column ${column} integer not null default 0;
1044
+ END
1045
+ return <<END if opts[:index]
1046
+ create index ${index} on ${table}(${column});
1047
+ END
1048
+ return <<END if opts[:unique]
1049
+ create unique index ${unique} on ${table}(${column});
1050
+ --alter table ${table} add constraint ${unique} unique (${column});
1051
+ END
1052
+ return <<END
1053
+ ---
1054
+ --- create table or index
1055
+ ---
1056
+ create table ${table} (
1057
+ id serial primary key,
1058
+ version integer not null default 0,
1059
+ name varchar(255) not null unique,
1060
+ created_at timestamp not null default current_timestamp,
1061
+ updated_at timestamp,
1062
+ deleted_at timestamp
1063
+ );
1064
+ create index ${index} on ${table}(${column});
1065
+ ---
1066
+ --- add column or unique constraint
1067
+ ---
1068
+ alter table ${table} add column ${column} varchar(255) not null unique;
1069
+ alter table ${table} add constraint ${unique} unique(${column});
1070
+ ---
1071
+ --- change column
1072
+ ---
1073
+ alter table ${table} rename column ${column} to ${new_column};
1074
+ alter table ${table} alter column ${column} type varchar(255);
1075
+ alter table ${table} alter column ${column} set not null;
1076
+ alter table ${table} alter column ${column} set default current_date;
1077
+ END
1078
+ end
1079
+
1080
+ def _section_down(mig, opts)
1081
+ return <<END if opts[:table]
1082
+ drop table ${table};
1083
+ END
1084
+ return <<END if opts[:column]
1085
+ alter table ${table} drop column ${column};
1086
+ END
1087
+ return <<END if opts[:index]
1088
+ drop index ${index};
1089
+ END
1090
+ return <<END if opts[:unique]
1091
+ drop index ${unique};
1092
+ --alter table ${table} drop constraint ${unique};
1093
+ END
1094
+ return <<END
1095
+ ---
1096
+ --- drop table or index
1097
+ ---
1098
+ drop table ${table};
1099
+ drop index ${index};
1100
+ ---
1101
+ --- drop column or unique constraint
1102
+ ---
1103
+ alter table ${table} drop column ${column};
1104
+ alter table ${table} drop constraint ${unique};
1105
+ ---
1106
+ --- revert column
1107
+ ---
1108
+ alter table ${table} rename column ${new_column} to ${column};
1109
+ alter table ${table} alter column ${column} type varchar(255);
1110
+ alter table ${table} alter column ${column} drop not null;
1111
+ alter table ${table} alter column ${column} drop default;
1112
+ END
1113
+ end
1114
+
1115
+ end
1116
+
1117
+ end
1118
+
1119
+
1120
+ class MySQL < Base
1121
+ SYMBOL = 'mysql'
1122
+ PATTERN = /\bmysql\b/
1123
+
1124
+ def execute_sql(sql, cmdopt=nil)
1125
+ preamble = ""
1126
+ sql = sql.gsub(/^-----/, '-- --')
1127
+ return super(preamble+sql, cmdopt)
1128
+ end
1129
+
1130
+ def run_sql(sql, opts={})
1131
+ preamble = ""
1132
+ sql = sql.gsub(/^-----/, '-- --')
1133
+ return super(preamble+sql, opts)
1134
+ end
1135
+
1136
+ protected
1137
+
1138
+ def _history_table_statement()
1139
+ sql = super
1140
+ sql = sql.sub(/PRIMARY KEY/, 'PRIMARY KEY AUTO_INCREMENT')
1141
+ #sql = sql.sub(' TIMESTAMP ', ' DATETIME ') # not work
1142
+ return sql
1143
+ end
1144
+
1145
+ def _execute_sql_and_get_column_as_text(sql)
1146
+ #cmdopt = "-s"
1147
+ #s = execute_sql(sql, cmdopt)
1148
+ #s.gsub!(/[^\\]\\n/, "\n")
1149
+ cmdopt = "-s -E"
1150
+ s = execute_sql(sql, cmdopt)
1151
+ s.sub!(/\A\*+.*\n/, '') # remove '**** 1. row ****' from output
1152
+ s.sub!(/\A\w+: /, '') # remove 'column-name: ' from output
1153
+ s.sub!(/\r?\n\z/, '') # remove "\n" at end
1154
+ return s
1155
+ end
1156
+
1157
+ def _echo_message(msg)
1158
+ return %Q`select "#{msg.to_s.gsub(/"/, '\\"')}" as '';`
1159
+ end
1160
+
1161
+ public
1162
+
1163
+ def q(str)
1164
+ return str.gsub(/[\\']/, '\\\\\&')
1165
+ end
1166
+
1167
+ def history_table_exist?
1168
+ table = history_table()
1169
+ output = execute_sql("show tables like '#{table}';")
1170
+ return output.include?(table)
1171
+ end
1172
+
1173
+ def get_migrations()
1174
+ migrations = _get_migrations("-s", /\t/)
1175
+ return migrations
1176
+ end
1177
+
1178
+ class Skeleton < BaseSkeleton
1179
+
1180
+ protected
1181
+
1182
+ def _section_vars(mig, opts)
1183
+ super
1184
+ end
1185
+
1186
+ def _section_up(mig, opts)
1187
+ return <<END if opts[:table]
1188
+ create table ${table} (
1189
+ id integer primary key auto_increment,
1190
+ version integer not null default 0,
1191
+ name varchar(255) not null,
1192
+ created_at timestamp not null default current_timestamp,
1193
+ updated_at timestamp,
1194
+ deleted_at timestamp
1195
+ ) engine=InnoDB default charset=utf8;
1196
+ -- alter table ${table} add index (name);
1197
+ -- alter table ${table} add index (created_at);
1198
+ -- alter table ${table} add index col1_col2_col3_idx(col1, col2, col3);
1199
+ -- alter table ${table} add unique (name);
1200
+ -- alter table ${table} add index col1_col2_col3_unq(col1, col2, col3);
1201
+ END
1202
+ return <<END if opts[:column]
1203
+ alter table ${table} add column ${column} integer not null default 0;
1204
+ END
1205
+ return <<END if opts[:index]
1206
+ alter table ${table} add index (${column});
1207
+ -- create index ${index} on ${table}(${column});
1208
+ END
1209
+ return <<END if opts[:unique]
1210
+ alter table ${table} add unique (${column});
1211
+ -- alter table ${table} add constraint ${unique} unique (${column});
1212
+ END
1213
+ return <<END
1214
+ --
1215
+ -- create table or index
1216
+ --
1217
+ create table ${table} (
1218
+ id integer primary key auto_increment,
1219
+ version integer not null default 0,
1220
+ name varchar(255) not null unique,
1221
+ created_at datetime not null default current_timestamp,
1222
+ updated_at datetime,
1223
+ deleted_at datetime
1224
+ );
1225
+ create index ${index} on ${table}(${column});
1226
+ --
1227
+ -- add column or unique constraint
1228
+ --
1229
+ alter table ${table} add column ${column} varchar(255) not null unique;
1230
+ alter table ${table} add constraint ${unique} unique(${column});
1231
+ --
1232
+ -- change column
1233
+ --
1234
+ alter table ${table} change column ${column} new_${column} integer not null;
1235
+ alter table ${table} modify column ${column} varchar(255) not null;
1236
+ END
1237
+ end
1238
+
1239
+ def _section_down(mig, opts)
1240
+ return <<END if opts[:table]
1241
+ drop table ${table};
1242
+ END
1243
+ return <<END if opts[:column]
1244
+ alter table ${table} drop column ${column};
1245
+ END
1246
+ return <<END if opts[:index]
1247
+ alter table ${table} drop index ${column};
1248
+ --alter table ${table} drop index ${index};
1249
+ END
1250
+ return <<END if opts[:unique]
1251
+ alter table ${table} drop index ${column};
1252
+ --alter table ${table} drop index ${unique};
1253
+ END
1254
+ return <<END
1255
+ --
1256
+ -- drop table or index
1257
+ --
1258
+ drop table ${table};
1259
+ drop index ${index};
1260
+ --
1261
+ -- drop column or unique constraint
1262
+ --
1263
+ alter table ${table} drop column ${column};
1264
+ alter table ${table} drop constraint ${unique};
1265
+ --
1266
+ -- revert column
1267
+ --
1268
+ alter table ${table} change column new_${column} ${column} varchar(255);
1269
+ alter table ${table} modify column ${column} varchar(255) not null;
1270
+ END
1271
+ end
1272
+
1273
+ end
1274
+
1275
+ end
1276
+
1277
+
1278
+ end
1279
+
1280
+
1281
+ module Actions
1282
+
1283
+
1284
+ class Action
1285
+ NAME = nil
1286
+ DESC = nil
1287
+ OPTS = []
1288
+ ARGS = nil
1289
+
1290
+ def parser
1291
+ name = self.class.const_get(:NAME)
1292
+ opts = self.class.const_get(:OPTS)
1293
+ parser = Util::CommandOptionParser.new("#{name}:")
1294
+ parser.add("-h, --help:")
1295
+ opts.each {|cmdopt| parser.add(cmdopt) }
1296
+ return parser
1297
+ end
1298
+
1299
+ def parse(args)
1300
+ return parser().parse(args)
1301
+ end
1302
+
1303
+ def usage
1304
+ klass = self.class
1305
+ name = klass.const_get(:NAME)
1306
+ args = klass.const_get(:ARGS)
1307
+ desc = klass.const_get(:DESC)
1308
+ s = args ? "#{name} #{args}" : "#{name}"
1309
+ head = "#{File.basename($0)} #{s} : #{desc}\n"
1310
+ return head+parser().usage(20, ' ')
1311
+ end
1312
+
1313
+ def short_usage()
1314
+ klass = self.class
1315
+ name = klass.const_get(:NAME)
1316
+ args = klass.const_get(:ARGS)
1317
+ desc = klass.const_get(:DESC)
1318
+ s = args ? "#{name} #{args}" : "#{name}"
1319
+ return " %-20s: %s\n" % [s, desc]
1320
+ end
1321
+
1322
+ def run(options, args)
1323
+ raise NotImplementedError.new("#{self.class.name}#run(): not implemented yet.")
1324
+ end
1325
+
1326
+ def cmdopterr(*args)
1327
+ return Util::CommandOptionError.new(*args)
1328
+ end
1329
+
1330
+ def get_command
1331
+ cmd = ENV['MIGR8_COMMAND'] || ''
1332
+ ! cmd.empty? or
1333
+ raise CommandSetupError.new(<<END)
1334
+ ##
1335
+ ## ERROR: $MIGR8_COMMAND is empty. Please set it at first.
1336
+ ## Example: (MacOSX, Unix)
1337
+ ## $ export MIGR8_COMMAND='sqlite3 dbname' # for SQLite3
1338
+ ## # or 'psql -q -U user dbname' # for PosgreSQL
1339
+ ## # or 'mysql -s -u user dbname' # for MySQL
1340
+ ## Example: (Windows)
1341
+ ## C:\\> set MIGR8_COMMAND='sqlite3 dbname' # for SQLite3
1342
+ ## # or 'psql -q -U user dbname' # for PostgreSQL
1343
+ ## # or 'mysql -s -u user dbname' # for MySQL
1344
+ ##
1345
+ ## Run '#{File.basename($0)} readme' for details.
1346
+ ##
1347
+ END
1348
+ return cmd
1349
+ end
1350
+
1351
+ def repository(dbms=nil)
1352
+ return @repository || begin
1353
+ cmd = get_command()
1354
+ dbms = DBMS.detect_by_command(cmd)
1355
+ $MIGR8_DBMS = dbms # TODO: remove if possible
1356
+ repo = Repository.new(dbms)
1357
+ _check(repo, dbms) if _should_check?
1358
+ repo
1359
+ end
1360
+ end
1361
+
1362
+ private
1363
+
1364
+ def _should_check? # :nodoc:
1365
+ true
1366
+ end
1367
+
1368
+ def _check(repo, dbms) # :nodoc:
1369
+ script = File.basename($0)
1370
+ unless dbms.history_table_exist?
1371
+ $stderr << <<END
1372
+ ##
1373
+ ## ERROR: history table not created.
1374
+ ## (Please run '#{script} readme' or '#{script} init' at first.)
1375
+ ##
1376
+ END
1377
+ raise RepositoryError.new("#{dbms.history_table}: table not found.")
1378
+ end
1379
+ unless repo.history_file_exist?
1380
+ $stderr << <<END
1381
+ ##
1382
+ ## ERROR: history file not found.
1383
+ ## (Please run '#{script} readme' or '#{script} init' at first.)
1384
+ ##
1385
+ END
1386
+ raise RepositoryError.new("#{repo.history_filepath}: not found.")
1387
+ end
1388
+ end
1389
+
1390
+ public
1391
+
1392
+ @subclasses = []
1393
+
1394
+ def self.inherited(subclass)
1395
+ @subclasses << subclass
1396
+ end
1397
+
1398
+ def self.subclasses
1399
+ @subclasses
1400
+ end
1401
+
1402
+ def self.find_by_name(name)
1403
+ return @subclasses.find {|cls| cls.const_get(:NAME) == name }
1404
+ end
1405
+
1406
+ protected
1407
+
1408
+ def _wrap # :nodoc:
1409
+ begin
1410
+ yield
1411
+ rescue MigrationError => ex
1412
+ name = self.class.const_get(:NAME)
1413
+ raise cmdopterr("#{name}: #{ex.message}")
1414
+ end
1415
+ end
1416
+
1417
+ def _recommend_to_set_MIGR8_EDITOR(action) # :nodoc:
1418
+ msg = <<END
1419
+ ##
1420
+ ## ERROR: Failed to #{action} migration file.
1421
+ ## Plase set $MIGR8_EDITOR in order to open migration file automatically.
1422
+ ## Example:
1423
+ ## $ export MIGR8_EDITOR='emacsclient' # for emacs
1424
+ ## $ export MIGR8_EDITOR='vim' # for vim
1425
+ ## $ export MIGR8_EDITOR='open -a TextMate' # for TextMate (MacOSX)
1426
+ ##
1427
+ END
1428
+ $stderr << msg
1429
+ end
1430
+
1431
+ end
1432
+
1433
+
1434
+ class ReadMeAction < Action
1435
+ NAME = "readme"
1436
+ DESC = "!!READ ME AT FIRST!!"
1437
+ OPTS = []
1438
+ ARGS = nil
1439
+
1440
+ attr_accessor :forced
1441
+
1442
+ def run(options, args)
1443
+ puts README
1444
+ end
1445
+
1446
+ end
1447
+
1448
+
1449
+ class HelpAction < Action
1450
+ NAME = "help"
1451
+ DESC = "show help message of action, or list action names"
1452
+ OPTS = []
1453
+ ARGS = '[action]'
1454
+
1455
+ def run(options, args)
1456
+ if args.length >= 2
1457
+ raise cmdopterr("help: too much argument")
1458
+ elsif args.length == 1
1459
+ action_name = args[0]
1460
+ action_class = Action.find_by_name(action_name) or
1461
+ raise cmdopterr("#{action_name}: unknown action.")
1462
+ puts action_class.new.usage()
1463
+ else
1464
+ usage = Migr8::Application.new.usage()
1465
+ puts usage
1466
+ end
1467
+ nil
1468
+ end
1469
+
1470
+ private
1471
+
1472
+ def _should_check?
1473
+ false
1474
+ end
1475
+
1476
+ end
1477
+
1478
+
1479
+ class InitAction < Action
1480
+ NAME = "init"
1481
+ DESC = "create necessary files and a table"
1482
+ OPTS = []
1483
+ ARGS = nil
1484
+
1485
+ def run(options, args)
1486
+ repository().init()
1487
+ end
1488
+
1489
+ private
1490
+
1491
+ def _should_check?
1492
+ false
1493
+ end
1494
+
1495
+ end
1496
+
1497
+
1498
+ class HistAction < Action
1499
+ NAME = "hist"
1500
+ DESC = "list history of versions"
1501
+ OPTS = ["-o: open history file with $MIGR8_EDITOR",
1502
+ "-b: rebuild history file from migration files"]
1503
+ ARGS = nil
1504
+
1505
+ def run(options, args)
1506
+ open_p = options['o']
1507
+ build_p = options['b']
1508
+ #
1509
+ if open_p
1510
+ editor = ENV['MIGR8_EDITOR']
1511
+ if ! editor || editor.empty?
1512
+ $stderr << "ERROR: $MIGR8_EDITOR is not set.\n"
1513
+ raise cmdopterr("#{NAME}: failed to open history file.")
1514
+ end
1515
+ histfile = repository().history_filepath()
1516
+ puts "$ #{editor} #{histfile}"
1517
+ system("#{editor} #{histfile}")
1518
+ return
1519
+ end
1520
+ #
1521
+ if build_p
1522
+ repo = repository()
1523
+ puts "## rebulding '#{repo.history_filepath()}' ..."
1524
+ repo.rebuild_history_file()
1525
+ puts "## done."
1526
+ return
1527
+ end
1528
+ #
1529
+ op = RepositoryOperation.new(repository())
1530
+ puts op.history
1531
+ end
1532
+
1533
+ end
1534
+
1535
+
1536
+ class NewAction < Action
1537
+ NAME = "new"
1538
+ DESC = "create new migration file and open it by $MIGR8_EDITOR"
1539
+ OPTS = [
1540
+ "-m text : description message (mandatory)",
1541
+ "-u user : author name (default: current user)",
1542
+ "-v version : specify version number instead of random string",
1543
+ "-p : plain skeleton",
1544
+ "-e editor: editr command (such as 'emacsclient', 'open', ...)",
1545
+ "--table=table : skeleton to create table",
1546
+ "--column=tbl.column : skeleton to add column",
1547
+ "--index=tbl.column : skeleton to create index",
1548
+ "--unique=tbl.column : skeleton to add unique constraint",
1549
+ ]
1550
+ ARGS = nil
1551
+
1552
+ def run(options, args)
1553
+ editor = options['e'] || ENV['MIGR8_EDITOR']
1554
+ if ! editor || editor.empty?
1555
+ _recommend_to_set_MIGR8_EDITOR('create')
1556
+ raise cmdopterr("#{NAME}: failed to create migration file.")
1557
+ end
1558
+ author = options['u']
1559
+ version = options['v']
1560
+ opts = {}
1561
+ opts[:plain] = true if options['p']
1562
+ desc = nil
1563
+ tblcol_rexp = /\A(\w+)(?:\.(\w+)|\((\w+)\))\z/
1564
+ if (val = options['table'])
1565
+ val =~ /\A(\w+)\z/ or
1566
+ raise cmdopterr("#{NAME} --table=#{val}: unexpected format.")
1567
+ desc = "create '#{$1}' table"
1568
+ opts[:table] = val
1569
+ end
1570
+ if (val = options['column'])
1571
+ val =~ tblcol_rexp or
1572
+ raise cmdopterr("#{NAME} --column=#{val}: unexpected format.")
1573
+ desc = "add '#{$2||$3}' column on '#{$1}' table"
1574
+ opts[:column] = val
1575
+ end
1576
+ if (val = options['index'])
1577
+ val =~ tblcol_rexp or
1578
+ raise cmdopterr("#{NAME} --index=#{val}: unexpected format.")
1579
+ desc = "create index on '#{$1}.#{$2||$3}'"
1580
+ opts[:index] = val
1581
+ end
1582
+ if (val = options['unique'])
1583
+ val =~ tblcol_rexp or
1584
+ raise cmdopterr("#{NAME} --unique=#{val}: unexpected format.")
1585
+ desc = "add unique constraint to '#{$1}.#{$2||$3}'"
1586
+ opts[:unique] = val
1587
+ end
1588
+ desc = options['m'] if options['m']
1589
+ desc or
1590
+ raise cmdopterr("#{NAME}: '-m text' option required.")
1591
+ #
1592
+ op = RepositoryOperation.new(repository())
1593
+ mig = _wrap { op.new(version, author, desc, opts) }
1594
+ puts "## New migration file:"
1595
+ puts mig.filepath
1596
+ puts "$ #{editor} #{mig.filepath}"
1597
+ system("#{editor} #{mig.filepath}")
1598
+ end
1599
+
1600
+ end
1601
+
1602
+
1603
+ class ShowAction < Action
1604
+ NAME = "show"
1605
+ DESC = "show migration file with expanding variables"
1606
+ OPTS = ["-x: load values of migration from history table in DB"]
1607
+ ARGS = "[version]"
1608
+
1609
+ def run(options, args)
1610
+ load_from_db = options['x']
1611
+ args.length <= 1 or
1612
+ raise cmdopterr("#{NAME}: too much arguments.")
1613
+ version = args.first # nil when args is empty
1614
+ #
1615
+ repo = repository()
1616
+ op = RepositoryOperation.new(repo)
1617
+ _wrap do
1618
+ puts op.show(version, load_from_db)
1619
+ end
1620
+ end
1621
+
1622
+ end
1623
+
1624
+
1625
+ class EditAction < Action
1626
+ NAME = "edit"
1627
+ DESC = "open migration file by $MIGR8_EDITOR"
1628
+ OPTS = [
1629
+ "-r N : edit N-th file from latest version",
1630
+ "-e editor : editr command (such as 'emacsclient', 'open', ...)",
1631
+ ]
1632
+ ARGS = "[version]"
1633
+
1634
+ def run(options, args)
1635
+ editor = options['e'] || ENV['MIGR8_EDITOR']
1636
+ if ! editor || editor.empty?
1637
+ _recommend_to_set_MIGR8_EDITOR('edit')
1638
+ raise cmdopterr("#{NAME}: failed to create migration file.")
1639
+ end
1640
+ version = num = nil
1641
+ if options['r']
1642
+ num = options['r'].to_i
1643
+ else
1644
+ if args.length == 0
1645
+ #raise cmdopterr("#{NAME}: '-r N' option or version required.")
1646
+ num = 1
1647
+ elsif args.length > 1
1648
+ raise cmdopterr("#{NAME}: too much arguments.")
1649
+ elsif args.length == 1
1650
+ version = args.first
1651
+ else
1652
+ raise "** unreachable"
1653
+ end
1654
+ end
1655
+ #
1656
+ repo = repository()
1657
+ if num
1658
+ migs = repo.migrations_in_history_file()
1659
+ mig = migs[-num] or
1660
+ raise cmdopterr("#{NAME} -n #{num}: migration file not found.")
1661
+ version = mig.version
1662
+ else
1663
+ mig = repo.load_migration(version) or
1664
+ raise cmdopterr("#{NAME}: #{version}: version not found.")
1665
+ end
1666
+ puts "# #{editor} #{repo.migration_filepath(version)}"
1667
+ system("#{editor} #{repo.migration_filepath(version)}")
1668
+ end
1669
+
1670
+ end
1671
+
1672
+
1673
+ class StatusAction < Action
1674
+ NAME = "status"
1675
+ DESC = "show status"
1676
+ OPTS = ["-n N : show N histories (default: 5)"]
1677
+ ARGS = nil
1678
+
1679
+ def run(options, args)
1680
+ if options['n']
1681
+ n = options['n'].to_i
1682
+ else
1683
+ n = 5
1684
+ end
1685
+ #
1686
+ op = RepositoryOperation.new(repository())
1687
+ puts op.status
1688
+ end
1689
+
1690
+ end
1691
+
1692
+
1693
+ class UpAction < Action
1694
+ NAME = "up"
1695
+ DESC = "apply next migration"
1696
+ OPTS = [
1697
+ "-n N : apply N migrations",
1698
+ "-a : apply all migrations",
1699
+ ]
1700
+ ARGS = nil
1701
+
1702
+ def run(options, args)
1703
+ if options['n']
1704
+ n = options['n'].to_i
1705
+ elsif options['a']
1706
+ n = nil
1707
+ else
1708
+ n = 1
1709
+ end
1710
+ #
1711
+ op = RepositoryOperation.new(repository())
1712
+ _wrap do
1713
+ op.upgrade(n)
1714
+ end
1715
+ end
1716
+
1717
+ end
1718
+
1719
+
1720
+ class DownAction < Action
1721
+ NAME = "down"
1722
+ DESC = "unapply current migration"
1723
+ OPTS = [
1724
+ "-n N : unapply N migrations",
1725
+ "--ALL : unapply all migrations",
1726
+ ]
1727
+ ARGS = nil
1728
+
1729
+ def run(options, args)
1730
+ n = 1
1731
+ if options['n']
1732
+ n = options['n'].to_i
1733
+ elsif options['ALL']
1734
+ n = nil
1735
+ end
1736
+ #
1737
+ op = RepositoryOperation.new(repository())
1738
+ _wrap do
1739
+ op.downgrade(n)
1740
+ end
1741
+ end
1742
+
1743
+ end
1744
+
1745
+
1746
+ class RedoAction < Action
1747
+ NAME = "redo"
1748
+ DESC = "do migration down, and up it again"
1749
+ OPTS = [
1750
+ "-n N : redo N migrations",
1751
+ "--ALL : redo all migrations",
1752
+ ]
1753
+ ARGS = nil
1754
+
1755
+ def run(options, args)
1756
+ n = 1
1757
+ if options['n']
1758
+ n = options['n'].to_i
1759
+ elsif options['ALL']
1760
+ n = nil
1761
+ end
1762
+ #
1763
+ op = RepositoryOperation.new(repository())
1764
+ _wrap do
1765
+ op.upgrade(n)
1766
+ op.downgrade(n)
1767
+ end
1768
+ end
1769
+
1770
+ end
1771
+
1772
+
1773
+ class ApplyAction < Action
1774
+ NAME = "apply"
1775
+ DESC = "apply specified migrations"
1776
+ OPTS = []
1777
+ ARGS = "version ..."
1778
+
1779
+ def run(options, args)
1780
+ ! args.empty? or
1781
+ raise cmdopterr("#{NAME}: version required.")
1782
+ #
1783
+ versions = args
1784
+ repo = repository()
1785
+ op = RepositoryOperation.new(repo)
1786
+ _wrap do
1787
+ op.apply(versions)
1788
+ end
1789
+ end
1790
+
1791
+ end
1792
+
1793
+
1794
+ class UnapplyAction < Action
1795
+ NAME = "unapply"
1796
+ DESC = "unapply specified migrations"
1797
+ OPTS = ["-x: unapply versions with down-script in DB, not in file"]
1798
+ ARGS = "version ..."
1799
+
1800
+ def run(options, args)
1801
+ only_in_db = options['x']
1802
+ ! args.empty? or
1803
+ raise cmdopterr("#{NAME}: version required.")
1804
+ #
1805
+ versions = args
1806
+ repo = repository()
1807
+ op = RepositoryOperation.new(repo)
1808
+ _wrap do
1809
+ if only_in_db
1810
+ op.unapply_only_in_database(versions)
1811
+ else
1812
+ op.unapply(versions)
1813
+ end
1814
+ end
1815
+ end
1816
+
1817
+ end
1818
+
1819
+
1820
+ class DeleteAction < Action
1821
+ NAME = "delete"
1822
+ DESC = "delete unapplied migration file"
1823
+ OPTS = ["--Imsure: you must specify this option to delete migration"]
1824
+ ARGS = "version ..."
1825
+
1826
+ def run(options, args)
1827
+ versions = args
1828
+ ! args.empty? or
1829
+ raise cmdopterr("#{NAME}: version required.")
1830
+ options['Imsure'] or
1831
+ raise cmdopterr("#{NAME}: you must specify '--Imsure' option.")
1832
+ #
1833
+ repo = repository()
1834
+ op = RepositoryOperation.new(repo)
1835
+ _wrap do
1836
+ versions.each do |version|
1837
+ print "## deleting '#{repo.migration_filepath(version)}' ... "
1838
+ begin
1839
+ op.delete(version)
1840
+ puts "done."
1841
+ rescue Exception => ex
1842
+ puts ""
1843
+ raise ex
1844
+ end
1845
+ end
1846
+ end
1847
+ end
1848
+
1849
+ end
1850
+
1851
+
1852
+ end
1853
+
1854
+
1855
+ class Application
1856
+
1857
+ def run(args)
1858
+ parser = new_cmdopt_parser()
1859
+ options = parser.parse(args) # may raise CommandOptionError
1860
+ #; [!dcggy] sets Migr8::DEBUG=true when '-d' or '--debug' specified.
1861
+ if options['debug']
1862
+ ::Migr8.DEBUG = true
1863
+ end
1864
+ #; [!ktlay] prints help message and exit when '-h' or '--help' specified.
1865
+ if options['help']
1866
+ $stdout << self.usage(parser)
1867
+ return 0
1868
+ end
1869
+ #; [!n0ubh] prints version string and exit when '-v' or '--version' specified.
1870
+ if options['version']
1871
+ $stdout << RELEASE << "\n"
1872
+ return 0
1873
+ end
1874
+ #;
1875
+ action_name = args.shift || default_action_name()
1876
+ action_class = Actions::Action.find_by_name(action_name) or
1877
+ raise Util::CommandOptionError.new("#{action_name}: unknown action.")
1878
+ action_obj = action_class.new
1879
+ action_opts = action_obj.parse(args)
1880
+ if action_opts['help']
1881
+ puts action_obj.usage
1882
+ else
1883
+ action_obj.run(action_opts, args)
1884
+ end
1885
+ #; [!saisg] returns 0 as status code when succeeded.
1886
+ return 0
1887
+ end
1888
+
1889
+ def usage(parser=nil)
1890
+ parser ||= new_cmdopt_parser()
1891
+ script = File.basename($0)
1892
+ s = ""
1893
+ s << "#{script} -- database schema version management tool\n"
1894
+ s << "\n"
1895
+ s << "Usage: #{script} [global-options] [action [options] [...]]\n"
1896
+ s << parser.usage(20, ' ')
1897
+ s << "\n"
1898
+ s << "Actions: (default: #{default_action_name()})\n"
1899
+ Migr8::Actions::Action.subclasses.each do |action_class|
1900
+ s << action_class.new.short_usage()
1901
+ end
1902
+ s << "\n"
1903
+ s << "(ATTENTION!! Run '#{script} readme' at first if you don't know #{script} well.)\n"
1904
+ s << "\n"
1905
+ return s
1906
+ end
1907
+
1908
+ def self.main(args=nil)
1909
+ #; [!cy0yo] uses ARGV when args is not passed.
1910
+ args = ARGV if args.nil?
1911
+ app = self.new
1912
+ begin
1913
+ status = app.run(args)
1914
+ #; [!maomq] command-option error is cached and not raised.
1915
+ rescue Util::CommandOptionError => ex
1916
+ script = File.basename($0)
1917
+ $stderr << "ERROR[#{script}] #{ex.message}\n"
1918
+ status = 1
1919
+ #;
1920
+ rescue Migr8Error => ex
1921
+ script = File.basename($0)
1922
+ $stderr << "ERROR[#{script}] #{ex}\n"
1923
+ status = 1
1924
+ end
1925
+ #; [!t0udo] returns status code (0: ok, 1: error).
1926
+ return status
1927
+ end
1928
+
1929
+ private
1930
+
1931
+ def new_cmdopt_parser
1932
+ parser = Util::CommandOptionParser.new
1933
+ parser.add("-h, --help: show help")
1934
+ parser.add("-v, --version: show version")
1935
+ parser.add("-D, --debug: not remove sql file ('migr8/tmp.sql') for debug")
1936
+ return parser
1937
+ end
1938
+
1939
+ def default_action_name
1940
+ readme_p = false
1941
+ readme_p = true if ENV['MIGR8_COMMAND'].to_s.strip.empty?
1942
+ readme_p = true if ! Repository.new(nil).history_file_exist?
1943
+ return readme_p ? 'readme' : 'status'
1944
+ end
1945
+
1946
+ end
1947
+
1948
+
1949
+ module Util
1950
+
1951
+
1952
+ class CommandOptionDefinitionError < StandardError
1953
+ end
1954
+
1955
+
1956
+ class CommandOptionError < StandardError
1957
+ end
1958
+
1959
+
1960
+ class CommandOptionDefinition
1961
+
1962
+ attr_accessor :short, :long, :arg, :name, :desc, :arg_required
1963
+
1964
+ def initialize(defstr)
1965
+ case defstr
1966
+ when /\A *--(\w[-\w]*)(?:\[=(.+?)\]|=(\S.*?))?(?:\s+\#(\w+))?\s*:(?:\s+(.*)?)?\z/
1967
+ short, long, arg, name, desc = nil, $1, ($2 || $3), $4, $5
1968
+ arg_required = $2 ? nil : $3 ? true : false
1969
+ when /\A *-(\w),\s*--(\w[-\w]*)(?:\[=(.+?)\]|=(\S.*?))?(?:\s+\#(\w+))?\s*:(?:\s+(.*)?)?\z/
1970
+ short, long, arg, name, desc = $1, $2, ($3 || $4), $5, $6
1971
+ arg_required = $3 ? nil : $4 ? true : false
1972
+ when /\A *-(\w)(?:\[(.+?)\]|\s+([^\#\s].*?))?(?:\s+\#(\w+))?\s*:(?:\s+(.*)?)?\z/
1973
+ short, long, arg, name, desc = $1, nil, ($2 || $3), $4, $5
1974
+ arg_required = $2 ? nil : $3 ? true : false
1975
+ else
1976
+ raise CommandOptionDefinitionError.new("'#{defstr}': invalid definition.")
1977
+ end
1978
+ name ||= (long || short)
1979
+ #
1980
+ @short = _strip(short)
1981
+ @long = _strip(long)
1982
+ @arg = _strip(arg)
1983
+ @name = _strip(name)
1984
+ @desc = _strip(desc)
1985
+ @arg_required = arg_required
1986
+ end
1987
+
1988
+ def usage(width=20)
1989
+ argreq = @arg_required
1990
+ if @short && @long
1991
+ s = "-#{@short}, --#{@long}" if argreq == false
1992
+ s = "-#{@short}, --#{@long}=#{@arg}" if argreq == true
1993
+ s = "-#{@short}, --#{@long}[=#{@arg}]" if argreq == nil
1994
+ elsif @long
1995
+ s = "--#{@long}" if argreq == false
1996
+ s = "--#{@long}=#{@arg}" if argreq == true
1997
+ s = "--#{@long}[=#{@arg}]" if argreq == nil
1998
+ elsif @short
1999
+ s = "-#{@short}" if argreq == false
2000
+ s = "-#{@short} #{@arg}" if argreq == true
2001
+ s = "-#{@short}[#{@arg}]" if argreq == nil
2002
+ end
2003
+ #; [!xd9do] returns option usage with specified width.
2004
+ return "%-#{width}s: %s" % [s, @desc]
2005
+ end
2006
+
2007
+ private
2008
+
2009
+ def _strip(str)
2010
+ return nil if str.nil?
2011
+ str = str.strip
2012
+ return str.empty? ? nil : str
2013
+ end
2014
+
2015
+ end
2016
+
2017
+
2018
+ class CommandOptionParser
2019
+
2020
+ attr_reader :optdefs
2021
+
2022
+ def initialize(prefix=nil)
2023
+ @prefix = prefix
2024
+ @optdefs = []
2025
+ end
2026
+
2027
+ def add(optdef)
2028
+ #; [!tm89j] parses definition string and adds optdef object.
2029
+ optdef = CommandOptionDefinition.new(optdef) if optdef.is_a?(String)
2030
+ @optdefs << optdef
2031
+ #; [!00kvl] returns self.
2032
+ return self
2033
+ end
2034
+
2035
+ def parse(args)
2036
+ options = {}
2037
+ while ! args.empty? && args[0] =~ /\A-/
2038
+ optstr = args.shift
2039
+ if optstr =~ /\A--/
2040
+ #; [!2jo9d] stops to parse options when '--' found.
2041
+ break if optstr == '--'
2042
+ #; [!7pa2x] raises error when invalid long option.
2043
+ optstr =~ /\A--(\w[-\w]+)(?:=(.*))?\z/ or
2044
+ raise cmdopterr("#{optstr}: invalid option format.")
2045
+ #; [!sj0cv] raises error when unknown long option.
2046
+ long, argval = $1, $2
2047
+ optdef = @optdefs.find {|x| x.long == long } or
2048
+ raise cmdopterr("#{optstr}: unknown option.")
2049
+ #; [!a7qxw] raises error when argument required but not provided.
2050
+ if optdef.arg_required == true && argval.nil?
2051
+ raise cmdopterr("#{optstr}: argument required.")
2052
+ #; [!8eu9s] raises error when option takes no argument but provided.
2053
+ elsif optdef.arg_required == false && argval
2054
+ raise cmdopterr("#{optstr}: unexpected argument.")
2055
+ end
2056
+ #; [!1l2dn] when argname is 'N'...
2057
+ if optdef.arg == 'N' && argval
2058
+ #; [!cfjp3] raises error when argval is not an integer.
2059
+ argval =~ /\A-?\d+\z/ or
2060
+ raise cmdopterr("#{optstr}: integer expected.")
2061
+ #; [!18p1g] raises error when argval <= 0.
2062
+ argval = argval.to_i
2063
+ argval > 0 or
2064
+ raise cmdopterr("#{optstr}: positive value expected.")
2065
+ end
2066
+ #; [!dtbdd] uses option name instead of long name when option name specified.
2067
+ #; [!7mp75] sets true as value when argument is not provided.
2068
+ options[optdef.name] = argval.nil? ? true : argval
2069
+ elsif optstr =~ /\A-/
2070
+ i = 1
2071
+ while i < optstr.length
2072
+ ch = optstr[i].chr
2073
+ #; [!8aaj0] raises error when unknown short option provided.
2074
+ optdef = @optdefs.find {|x| x.short == ch } or
2075
+ raise cmdopterr("-#{ch}: unknown option.")
2076
+ #; [!mnwxw] when short option takes no argument...
2077
+ if optdef.arg_required == false # no argument
2078
+ #; [!8atm1] sets true as value.
2079
+ options[optdef.name] = true
2080
+ i += 1
2081
+ #; [!l5mee] when short option takes required argument...
2082
+ elsif optdef.arg_required == true # required argument
2083
+ #; [!crvxx] uses following string as argument.
2084
+ argval = optstr[(i+1)..-1]
2085
+ if argval.empty?
2086
+ #; [!7t6l3] raises error when no argument provided.
2087
+ ! args.empty? or
2088
+ raise cmdopterr("-#{ch}: argument required.")
2089
+ argval = args.shift
2090
+ end
2091
+ #; [!h3gt8] when argname is 'N'...
2092
+ if optdef.arg == 'N'
2093
+ #; [!yzr2p] argument must be an integer.
2094
+ argval =~ /\A-?\d+\z/ or
2095
+ raise cmdopterr("-#{ch} #{argval}: integer expected.")
2096
+ #; [!mcwu7] argument must be positive value.
2097
+ argval = argval.to_i
2098
+ argval > 0 or
2099
+ raise cmdopterr("-#{ch} #{argval}: positive value expected.")
2100
+ end
2101
+ #
2102
+ options[optdef.name] = argval
2103
+ break
2104
+ #; [!pl97z] when short option takes optional argument...
2105
+ elsif optdef.arg_required == nil # optional argument
2106
+ #; [!4k3zy] uses following string as argument if provided.
2107
+ argval = optstr[(i+1)..-1]
2108
+ if argval.empty?
2109
+ #; [!9k2ip] uses true as argument value if not provided.
2110
+ argval = true
2111
+ end
2112
+ #; [!lk761] when argname is 'N'...
2113
+ if optdef.arg == 'N' && argval.is_a?(String)
2114
+ #; [!6oy04] argument must be an integer.
2115
+ argval =~ /\A-?\d+\z/ or
2116
+ raise cmdopterr("-#{ch}#{argval}: integer expected.")
2117
+ #; [!nc3av] argument must be positive value.
2118
+ argval = argval.to_i
2119
+ argval > 0 or
2120
+ raise cmdopterr("-#{ch}#{argval}: positive value expected.")
2121
+ end
2122
+ #
2123
+ options[optdef.name] = argval
2124
+ break
2125
+ else
2126
+ raise "** unreachable"
2127
+ end
2128
+ end#while
2129
+ end#if
2130
+ end#while
2131
+ #; [!35eof] returns parsed options.
2132
+ return options
2133
+ end#def
2134
+
2135
+ def usage(width=20, indent='')
2136
+ width = 20 if width.nil?
2137
+ #; [!w9v9c] returns usage string of all options.
2138
+ s = ""
2139
+ @optdefs.each do |optdef|
2140
+ #; [!i0uvr] adds indent when specified.
2141
+ #; [!lbjai] skips options when desc is empty.
2142
+ s << "#{indent}#{optdef.usage(width)}\n" if optdef.desc
2143
+ end
2144
+ return s
2145
+ end
2146
+
2147
+ private
2148
+
2149
+ def cmdopterr(message)
2150
+ message = "#{@prefix} #{message}" if @prefix
2151
+ return CommandOptionError.new(message)
2152
+ end
2153
+
2154
+ end#class
2155
+
2156
+
2157
+ module Expander
2158
+
2159
+ class UnknownVariableError < Migr8Error
2160
+ end
2161
+
2162
+ module_function
2163
+
2164
+ def expand_vars(vars)
2165
+ dict = {}
2166
+ vars.each do |d|
2167
+ d.each do |k, v|
2168
+ dict[k] = expand_value(v, dict)
2169
+ end
2170
+ end
2171
+ return dict
2172
+ end
2173
+
2174
+ def expand_value(value, dict)
2175
+ case value
2176
+ when String
2177
+ return expand_str(value, dict)
2178
+ when Array
2179
+ arr = value
2180
+ i = 0
2181
+ while i < arr.length
2182
+ arr[i] = expand_value(arr[i], dict)
2183
+ i += 1
2184
+ end
2185
+ return arr
2186
+ when Hash
2187
+ hash = value
2188
+ hash.keys.each do |k|
2189
+ hash[k] = expand_value(hash[k], dict)
2190
+ end
2191
+ return hash
2192
+ else
2193
+ return value
2194
+ end
2195
+ end
2196
+
2197
+ def expand_str(str, dict)
2198
+ raise unless dict.is_a?(Hash)
2199
+ if str =~ /\A\$\{(.*?)\}\z/
2200
+ var = $1
2201
+ if var.empty?
2202
+ return ''
2203
+ elsif dict.key?(var)
2204
+ return dict[var]
2205
+ else
2206
+ raise UnknownVariableError.new("${#{var}}: no such variable.")
2207
+ end
2208
+ else
2209
+ return str.gsub(/\$\{(.*?)\}/) {
2210
+ var = $1
2211
+ if var.empty?
2212
+ ''
2213
+ elsif dict.key?(var)
2214
+ dict[var].to_s
2215
+ else
2216
+ raise UnknownVariableError.new("${#{var}}: no such variable.")
2217
+ end
2218
+ }
2219
+ end
2220
+ end
2221
+
2222
+ end
2223
+
2224
+
2225
+ class Template
2226
+
2227
+ def initialize(input="")
2228
+ #; [!6z4kp] converts input string into ruby code.
2229
+ self.src = convert(input)
2230
+ end
2231
+
2232
+ attr_reader :src
2233
+
2234
+ def src=(src)
2235
+ @src = src
2236
+ @_proc = eval "proc { #{@src} }"
2237
+ end
2238
+
2239
+ def render(context={})
2240
+ #; [!umsfx] takes hash object as context variables.
2241
+ #; [!p0po0] context argument can be null.
2242
+ ctx = TemplateContext.new(context)
2243
+ #; [!48pfc] returns rendered string.
2244
+ #; [!1i0v8] escapes "'" into "''" when '<%= %>', and not when '<%== %>'.
2245
+ return ctx.instance_eval(&@_proc)
2246
+ end
2247
+
2248
+ EMBED_REXP = /(^[ \t]*)?<%(==?|\#)?(.*?)%>([ \t]*\r?\n)?/m
2249
+
2250
+ def convert(input)
2251
+ #; [!118pw] converts template string into ruby code.
2252
+ #; [!7ht59] escapes '`' and '\\' characters.
2253
+ src = "_buf = '';" # preamble
2254
+ pos = 0
2255
+ input.scan(EMBED_REXP) do |lspace, ch, code, rspace|
2256
+ match = Regexp.last_match
2257
+ text = input[pos...match.begin(0)]
2258
+ pos = match.end(0)
2259
+ src << _t(text)
2260
+ #; [!u93y5] wraps expression by 'escape()' when <%= %>.
2261
+ #; [!auj95] leave expression as it is when <%== %>.
2262
+ if ch == '=' # expression (escaping)
2263
+ src << _t(lspace) << " _buf << (escape(#{code})).to_s;" << _t(rspace)
2264
+ elsif ch == '==' # expression (without escaping)
2265
+ src << _t(lspace) << " _buf << (#{code}).to_s;" << _t(rspace)
2266
+ elsif ch == '#' # comment
2267
+ src << _t(lspace) << ("\n" * code.count("\n")) << _t(rspace)
2268
+ else # statement
2269
+ if lspace && rspace
2270
+ src << "#{lspace}#{code}#{rspace};"
2271
+ else
2272
+ src << _t(lspace) << code << ';' << _t(rspace)
2273
+ end
2274
+ end
2275
+ end
2276
+ #; [!b10ns] generates ruby code correctly even when no embedded code.
2277
+ rest = $' || input
2278
+ src << _t(rest)
2279
+ src << "\n_buf.to_s\n" # postamble
2280
+ return src
2281
+ end
2282
+
2283
+ private
2284
+
2285
+ def _build_text(text)
2286
+ return text && !text.empty? ? " _buf << %q`#{_escape_text(text)}`;" : ''
2287
+ end
2288
+ alias _t _build_text
2289
+
2290
+ def _escape_text(text)
2291
+ return text.gsub!(/[`\\]/, '\\\\\&') || text
2292
+ end
2293
+
2294
+ end
2295
+
2296
+
2297
+ class TemplateContext
2298
+
2299
+ def initialize(vars={})
2300
+ #; [!p69q1] takes vars and sets them into instance variables.
2301
+ #; [!p853f] do nothing when vars is nil.
2302
+ vars.each do |k, v|
2303
+ instance_variable_set("@#{k}", v)
2304
+ end if vars
2305
+ end
2306
+
2307
+ def escape(value)
2308
+ #; [!6v5yq] escapes "'" into "\\'" when on MySQL dbms.
2309
+ return $MIGR8_DBMS.q(value.to_s) if $MIGR8_DBMS
2310
+ #; [!f3yy9] escapes "'" into "''" for default.
2311
+ #; [!to5kz] converts any value into string.
2312
+ return value.to_s.gsub(/'/, "''")
2313
+ end
2314
+
2315
+ end
2316
+
2317
+
2318
+ end
2319
+
2320
+
2321
+ README = <<'README_DOCUMENT'
2322
+ Migr8.rb
2323
+ ========
2324
+
2325
+ Migr8.rb is a database schema version management tool.
2326
+
2327
+ * Easy to install, easy to setup, and easy to start
2328
+ * No configuration file; instead, only two environment variables
2329
+ * Designed carefully to suit Git or Mercurial
2330
+ * Supports SQLite3, PostgreSQL, and MySQL
2331
+ * Written in Ruby (>= 1.8)
2332
+
2333
+
2334
+ Quick Start
2335
+ -----------
2336
+
2337
+ 1. Donwload migr8.rb.
2338
+
2339
+ $ curl -Lo migr8.rb http://bit.ly/migr8_rb
2340
+ $ chmod a+x migr8.rb
2341
+ ### or
2342
+ $ gem install migr8
2343
+
2344
+ 2. Set environment variables: $MIGR8_COMMAND and $MIGR8_EDITOR.
2345
+
2346
+ $ export MIGR8_COMMAND="sqlite3 dbfile1" # for SQLite3
2347
+ $ export MIGR8_COMMAND="psql -q -U user1 dbname1" # for PostgreSQL
2348
+ $ export MIGR8_COMMAND="mysql -s -u user1 dbname1" # for MySQL
2349
+
2350
+ $ export MIGR8_EDITOR="open -a TextMate" # for TextMate (MacOSX)
2351
+ $ export MIGR8_EDITOR="emacsclient" # for Emacs
2352
+ $ export MIGR8_EDITOR="vim" # for Vim
2353
+
2354
+ 3. Create managiment files and table.
2355
+
2356
+ $ ./migr8.rb init # create files in current directory,
2357
+ # and create a table in DB.
2358
+
2359
+ 4. Now you can manage DB schema versions.
2360
+
2361
+ $ ./migr8.rb # show current status
2362
+ $ ./migr8.rb new -m "create 'users' table" # create a migration
2363
+ # or ./migr8.rb new --table=users
2364
+ $ ./migr8.rb # show status again
2365
+ $ ./migr8.rb up # apply migration
2366
+ $ ./migr8.rb # show status again
2367
+ $ ./migr8.rb hist # list history
2368
+
2369
+ 5. You may got confliction error when `git rebase` or `git pull`.
2370
+ In this case, you must resolve it by hand.
2371
+ (This is intended design.)
2372
+
2373
+ $ git rebase master # confliction!
2374
+ $ ./migr8.rb hist -o # open 'migr8/history.txt', and
2375
+ # resolve confliction manually
2376
+ $ ./migr8.rb hist # check whether history file is valid
2377
+ $ git add migr8/history.txt
2378
+ $ git rebase --continue
2379
+
2380
+
2381
+ Templating
2382
+ ----------
2383
+
2384
+ (!!Attention!! this is experimental feature and may be changed in the future.)
2385
+
2386
+ It is possible to embed eRuby code into `up` and `down` scripts.
2387
+
2388
+ Syntax:
2389
+
2390
+ * `<% ... %>` : Ruby statement
2391
+ * `<%= ... %>` : Ruby expression, escaping `'` into `''` (or `\'` on MySQL)
2392
+ * `<%== ... %>` : Ruby expression, no escaping
2393
+
2394
+ For example:
2395
+
2396
+ vars:
2397
+ - table: users
2398
+
2399
+ up: |
2400
+ insert into ${table}(name) values
2401
+ <% comma = " " %>
2402
+ <% for name in ["Haruhi", "Mikuru", "Yuki"] %>
2403
+ <%= comma %>('<%= name %>')
2404
+ <% comma = ", " %>
2405
+ <% end %>
2406
+ ;
2407
+
2408
+ down: |
2409
+ <% for name in ["Haruhi", "Mikuru", "Yuki"] %>
2410
+ delete from ${table} where name = '<%= name %>';
2411
+ <% end %>
2412
+
2413
+ The above is the same as the following:
2414
+
2415
+ up: |
2416
+ insert into users(name) values
2417
+ ('Haruhi')
2418
+ , ('Mikuru')
2419
+ , ('Yuki')
2420
+ ;
2421
+
2422
+ down: |
2423
+ delete from users where name = 'Haruhi';
2424
+ delete from users where name = 'Mikuru';
2425
+ delete from users where name = 'Yuki';
2426
+
2427
+ In eRuby code, values in `vars` are available as instance variables.
2428
+ For example:
2429
+
2430
+ version: uhtu4853
2431
+ desc: register members
2432
+ author: kyon
2433
+ vars:
2434
+ - table: users
2435
+ - members: [Haruhi, Mikuru, Yuki]
2436
+
2437
+ up: |
2438
+ <% for member in @members %>
2439
+ insert into ${table}(name) values ('<%= member %>');
2440
+ <% end %>
2441
+
2442
+ down: |
2443
+ <% for member in @members %>
2444
+ delete from ${table} where name = '<%= member %>';
2445
+ <% end %>
2446
+
2447
+ If you want to see up and down scripts rendered, run `migr8.rb show` action.
2448
+ For example:
2449
+
2450
+ $ ./migr8.rb show uhtu4853
2451
+ version: uhtu4853
2452
+ desc: register members
2453
+ author: kyon
2454
+ vars:
2455
+ - table: "users"
2456
+ - members: ["Haruhi", "Mikuru", "Yuki"]
2457
+
2458
+ up: |
2459
+ insert into users(name) values ('Haruhi');
2460
+ insert into users(name) values ('Mikuru');
2461
+ insert into users(name) values ('Yuki');
2462
+
2463
+ down: |
2464
+ delete from users where name = 'Haruhi';
2465
+ delete from users where name = 'Mikuru';
2466
+ delete from users where name = 'Yuki';
2467
+
2468
+
2469
+ Notice that migration file using eRuby code is not compatible with other
2470
+ Migr8 implemtation.
2471
+
2472
+
2473
+ Tips
2474
+ ----
2475
+
2476
+ * `migr8.rb up -a` applys all migrations, while `migr8.rb up` applys a
2477
+ migration.
2478
+
2479
+ * `migr8.rb -D up` saves SQL executed into `migr8/history.txt` file.
2480
+
2481
+ * `migr8.rb redo` is equivarent to `migr8.rb down; migr8.rb up`.
2482
+
2483
+ * `migr8.rb new -p` generates migration file with plain skeleton, and
2484
+ `migr8.rb new --table=name` generates with table name.
2485
+
2486
+ * `migr8.rb unapply -x` unapplies migration which is applied in DB but
2487
+ corresponding migration file doesn't exist.
2488
+ (Describing in detail, `migr8.rb unapply -x abcd1234` runs `down` script
2489
+ in `_migr_history` table, while `migr8.rb unapply abcd1234` runs `down`
2490
+ script in `migr8/migrations/abcd1234.yaml` file.)
2491
+ This may help you when switching Git/Hg branch.
2492
+
2493
+ * `migr8.rb` generates sql file and run it with sql command such as `psql`
2494
+ (PostgreSQL), `sqlite3` (SQLite3) or `mysql` (MySQL). Therefore you can
2495
+ use non-sql command in migration file.
2496
+ For example:
2497
+
2498
+ up: |
2499
+ -- read data from CSV file and insert into DB (PostgreSQL)
2500
+ \copy table1 from 'file1.csv' with csv;
2501
+
2502
+ * **MySQL doesn't support transactional DDL**.
2503
+ It will cause troubles when you have errors in migration script
2504
+ (See https://www.google.com/search?q=transactional+DDL for details).
2505
+ On the other hand, SQLite3 and PostgreSQL support transactional DDL,
2506
+ and DDL will be rollbacked when error occurred in migration script.
2507
+ Very good.
2508
+
2509
+
2510
+ Usage and Actions
2511
+ -----------------
2512
+
2513
+ Usage: migr8.rb [global-options] [action [options] [...]]
2514
+ -h, --help : show help
2515
+ -v, --version : show version
2516
+ -D, --debug : not remove sql file ('migr8/tmp.sql') for debug
2517
+
2518
+ Actions: (default: status)
2519
+ readme : !!READ ME AT FIRST!!
2520
+ help [action] : show help message of action, or list action names
2521
+ init : create necessary files and a table
2522
+ hist : list history of versions
2523
+ -o : open history file with $MIGR8_EDITOR
2524
+ -b : rebuild history file from migration files
2525
+ new : create new migration file and open it by $MIGR8_EDITOR
2526
+ -m text : description message (mandatory)
2527
+ -u user : author name (default: current user)
2528
+ -v version : specify version number instead of random string
2529
+ -p : plain skeleton
2530
+ -e editor : editr command (such as 'emacsclient', 'open', ...)
2531
+ --table=table : skeleton to create table
2532
+ --column=tbl.col : skeleton to add column
2533
+ --index=tbl.col : skeleton to create index
2534
+ --unique=tbl.col : skeleton to add unique constraint
2535
+ show [version] : show migration file with expanding variables
2536
+ -x : load values of migration from history table in DB
2537
+ edit [version] : open migration file by $MIGR8_EDITOR
2538
+ -r N : edit N-th file from latest version
2539
+ -e editor : editr command (such as 'emacsclient', 'open', ...)
2540
+ status : show status
2541
+ up : apply next migration
2542
+ -n N : apply N migrations
2543
+ -a : apply all migrations
2544
+ down : unapply current migration
2545
+ -n N : unapply N migrations
2546
+ --ALL : unapply all migrations
2547
+ redo : do migration down, and up it again
2548
+ -n N : redo N migrations
2549
+ --ALL : redo all migrations
2550
+ apply version ... : apply specified migrations
2551
+ unapply version ... : unapply specified migrations
2552
+ -x : unapply versions with down-script in DB, not in file
2553
+ delete version ... : delete unapplied migration file
2554
+ --Imsure : you must specify this option to delete migration
2555
+
2556
+
2557
+ TODO
2558
+ ----
2559
+
2560
+ * [_] write more tests
2561
+ * [_] test on windows
2562
+ * [_] implement in Python
2563
+ * [_] implement in JavaScript
2564
+
2565
+
2566
+ Changes
2567
+ -------
2568
+
2569
+ ### Release 0.4.0 (2013-11-28) ###
2570
+
2571
+ * [enhance] RubyGems package available.
2572
+ You can install migr8.rb by `gem install migr8`.
2573
+ * [enhance] eRuby templating `up` and `down` script.
2574
+ See 'Templating' section of README file for details.
2575
+ * [enhance] Add new action 'show' which shows migration attributes
2576
+ with expanding variables (ex: `${table}`) and renderting template.
2577
+ * [enhance] Add new action 'delete' which deletes unapplied migration file.
2578
+ Note: this action can't delete migration which is already applied.
2579
+ * [enhance] Add new option 'new -v version' in order to specify version
2580
+ number by yourself instead of auto-generated random string.
2581
+ * [bufix] Action 'edit version' now can open migration file even when
2582
+ version number in migration file is wrong.
2583
+
2584
+
2585
+ ### Release 0.3.1 (2013-11-24) ###
2586
+
2587
+ * [bugfix] Fix 'hist' action not to raise error.
2588
+
2589
+
2590
+ ### Release 0.3.0 (2013-11-22) ###
2591
+
2592
+ * [enhance] Add `-x` option to `unapply` action which unapplies migrations
2593
+ by down-script in DB, not in migration file.
2594
+ You can unapply migrations which files are missing in some reason.
2595
+ * [change] Eliminate indentation from output of 'readme' action.
2596
+
2597
+
2598
+ ### Release 0.2.1 (2013-11-20) ###
2599
+
2600
+ * [bugfix] Fix `new --table=name` action to set table name correctly
2601
+
2602
+
2603
+ ### Release 0.2.0 (2013-11-14) ###
2604
+
2605
+ * [enhance] Add new options to `new` action for some skeletons
2606
+ * `new --table=table` : create table
2607
+ * `new --column=tbl.col` : add column to table
2608
+ * `new --index=tbl.col` : create index on column
2609
+ * `new --unique=tbl.col` : add unique constraint on column
2610
+ * [enhance] Add new option `hist -b` action which re-generate history file.
2611
+ * [change] Change several error messages
2612
+ * [change] Tweak SQL generated on SQLite3
2613
+
2614
+
2615
+ ### Release 0.1.1 (2013-11-12) ###
2616
+
2617
+ * [IMPORTANT] Change history table schema: SORRY, YOU MUST RE-CREATE HISTORY TABLE.
2618
+ * [enhance] Fix 'up' action to save both up and down script into history table.
2619
+
2620
+
2621
+ ### Release 0.1.0 (2013-11-11) ###
2622
+
2623
+ * Public release
2624
+
2625
+
2626
+ License
2627
+ -------
2628
+
2629
+ $License: MIT License $
2630
+
2631
+
2632
+ Copyright
2633
+ ---------
2634
+
2635
+ $Copyright: copyright(c) 2013 kuwata-lab.com all rights reserved $
2636
+ README_DOCUMENT
2637
+
2638
+
2639
+ end
2640
+
2641
+
2642
+ if __FILE__ == $0
2643
+ status = Migr8::Application.main()
2644
+ exit(status)
2645
+ end