migr8 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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