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.
- data/MIT-LICENSE +20 -0
- data/README.md +356 -0
- data/Rakefile +133 -0
- data/bin/migr8.rb +14 -0
- data/lib/migr8.rb +2645 -0
- data/migr8.gemspec +57 -0
- data/setup.rb +1585 -0
- data/test/Application_test.rb +148 -0
- data/test/Migration_test.rb +261 -0
- data/test/Util_test.rb +873 -0
- data/test/helpers.rb +93 -0
- data/test/oktest.rb +1537 -0
- data/test/run_all.rb +8 -0
- metadata +71 -0
data/bin/migr8.rb
ADDED
@@ -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
|
data/lib/migr8.rb
ADDED
@@ -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
|