xmigra 1.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.html +232 -0
- data/README.md +113 -0
- data/Rakefile +6 -0
- data/bin/xmigra +5 -0
- data/lib/xmigra/version.rb +3 -0
- data/lib/xmigra.rb +3500 -0
- data/test/git_vcs.rb +242 -0
- data/test/runner.rb +129 -0
- data/xmigra.gemspec +28 -0
- metadata +90 -0
data/lib/xmigra.rb
ADDED
@@ -0,0 +1,3500 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
# Copyright 2013 by Next IT Corporation.
|
5
|
+
#
|
6
|
+
# This work is licensed under the Creative Commons Attribution-ShareAlike 4.0
|
7
|
+
# International License. To view a copy of this license, visit
|
8
|
+
# http://creativecommons.org/licenses/by-sa/4.0/.
|
9
|
+
|
10
|
+
require "digest/md5"
|
11
|
+
require "fileutils"
|
12
|
+
require "optparse"
|
13
|
+
require "ostruct"
|
14
|
+
require "pathname"
|
15
|
+
require "rbconfig"
|
16
|
+
require "rexml/document"
|
17
|
+
require "tsort"
|
18
|
+
require "yaml"
|
19
|
+
|
20
|
+
require "xmigra/version"
|
21
|
+
|
22
|
+
unless Object.instance_methods.include? :define_singleton_method
|
23
|
+
class Object
|
24
|
+
def define_singleton_method(name, &body)
|
25
|
+
metaclass = class << self; self; end
|
26
|
+
metaclass.send(:define_method, name, &body)
|
27
|
+
return method(name)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def Array.from_generator(proc)
|
33
|
+
result = new
|
34
|
+
proc.call {|item| result << item}
|
35
|
+
return result
|
36
|
+
end
|
37
|
+
|
38
|
+
class Array
|
39
|
+
def insert_after(element, new_element)
|
40
|
+
insert(index(element) + 1, new_element)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class Pathname
|
45
|
+
def glob(rel_path, *args, &block)
|
46
|
+
if block_given?
|
47
|
+
Pathname.glob(self + rel_path, *args) {|p| yield self + p}
|
48
|
+
else
|
49
|
+
Pathname.glob(self + rel_path, *args).map {|p| self + p}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Make YAML scalars dump back out in the same style they were when read in
|
55
|
+
if defined? YAML::Syck
|
56
|
+
class YAML::Syck::Node
|
57
|
+
alias_method :orig_transform_Lorjiardaik9, :transform
|
58
|
+
def transform
|
59
|
+
tv = orig_transform_Lorjiardaik9
|
60
|
+
if tv.kind_of? String and @style
|
61
|
+
node_style = @style
|
62
|
+
tv.define_singleton_method(:to_yaml_style) {node_style}
|
63
|
+
end
|
64
|
+
return tv
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
if defined? YAML::ENGINE.yamler
|
69
|
+
previous = YAML::ENGINE.yamler
|
70
|
+
YAML::ENGINE.yamler = 'syck'
|
71
|
+
YAML::ENGINE.yamler = previous
|
72
|
+
$xmigra_yamler = Syck
|
73
|
+
else
|
74
|
+
$xmigra_yamler = YAML
|
75
|
+
end
|
76
|
+
|
77
|
+
elsif defined? Psych
|
78
|
+
class Psych::Nodes::Scalar
|
79
|
+
alias_method :orig_transform_Lorjiardaik9, :transform
|
80
|
+
def transform
|
81
|
+
tv = orig_transform_Lorjiardaik9
|
82
|
+
if @style
|
83
|
+
node_style = @style
|
84
|
+
tv.define_singleton_method(:yaml_style) {node_style}
|
85
|
+
end
|
86
|
+
return tv
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
module YAMLRepro
|
91
|
+
class TreeBuilder < Psych::TreeBuilder
|
92
|
+
Scalar = ::Psych::Nodes::Scalar
|
93
|
+
|
94
|
+
attr_writer :next_collection_style
|
95
|
+
|
96
|
+
def initialize(*args)
|
97
|
+
super
|
98
|
+
@next_collection_style = nil
|
99
|
+
end
|
100
|
+
|
101
|
+
def next_collection_style(default_style)
|
102
|
+
style = @next_collection_style || default_style
|
103
|
+
@next_collection_style = nil
|
104
|
+
style
|
105
|
+
end
|
106
|
+
|
107
|
+
def scalar(value, anchor, tag, plain, quoted, style)
|
108
|
+
if style_any?(style) and value.respond_to?(:yaml_style) and style = value.yaml_style
|
109
|
+
if style_block_scalar?(style)
|
110
|
+
plain = false
|
111
|
+
quoted = true
|
112
|
+
end
|
113
|
+
end
|
114
|
+
super
|
115
|
+
end
|
116
|
+
|
117
|
+
def style_any?(style)
|
118
|
+
Scalar::ANY == style
|
119
|
+
end
|
120
|
+
|
121
|
+
def style_block_scalar?(style)
|
122
|
+
[Scalar::LITERAL, Scalar::FOLDED].include? style
|
123
|
+
end
|
124
|
+
|
125
|
+
%w[sequence mapping].each do |node_type|
|
126
|
+
class_eval <<-RUBY
|
127
|
+
def start_#{node_type}(anchor, tag, implicit, style)
|
128
|
+
style = next_collection_style(style)
|
129
|
+
super
|
130
|
+
end
|
131
|
+
RUBY
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Custom tree class to handle Hashes and Arrays tagged with `yaml_style`
|
136
|
+
class YAMLTree < Psych::Visitors::YAMLTree
|
137
|
+
%w[Hash Array Psych_Set Psych_Omap].each do |klass|
|
138
|
+
class_eval <<-RUBY
|
139
|
+
def visit_#{klass} o
|
140
|
+
if o.respond_to? :yaml_style
|
141
|
+
@emitter.next_sequence_or_mapping_style = o.yaml_style
|
142
|
+
end
|
143
|
+
super
|
144
|
+
end
|
145
|
+
RUBY
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def self.dump(data_root, io=nil, options={})
|
150
|
+
real_io = io || StringIO.new(''.encode('utf-8'))
|
151
|
+
visitor = YAMLTree.new(options, TreeBuilder.new)
|
152
|
+
visitor << data_root
|
153
|
+
ast = visitor.tree
|
154
|
+
|
155
|
+
begin
|
156
|
+
ast.yaml real_io
|
157
|
+
rescue
|
158
|
+
Psych::Visitors::Emitter.new(real_io).accept ast
|
159
|
+
end
|
160
|
+
|
161
|
+
io || real_io.string
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
$xmigra_yamler = YAMLRepro
|
166
|
+
|
167
|
+
else
|
168
|
+
$xmigra_yamler = YAML
|
169
|
+
end
|
170
|
+
|
171
|
+
|
172
|
+
module XMigra
|
173
|
+
FORMALIZATIONS = {
|
174
|
+
/xmigra/i=>"XMigra",
|
175
|
+
}
|
176
|
+
DBOBJ_NAME_SPLITTER = /^
|
177
|
+
(?:(\[[^\[\]]+\]|[^.\[]+)\.)? (?# Schema, match group 1)
|
178
|
+
(\[[^\[\]]+\]|[^.\[]+) (?# Object name, match group 2)
|
179
|
+
$/x
|
180
|
+
DBQUOTE_STRIPPER = /^\[?([^\]]+)\]?$/
|
181
|
+
PLATFORM = case
|
182
|
+
when (RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/) then :mswin
|
183
|
+
else :unix
|
184
|
+
end
|
185
|
+
|
186
|
+
class Error < RuntimeError; end
|
187
|
+
|
188
|
+
def self.canonize_path_case(s)
|
189
|
+
case PLATFORM
|
190
|
+
when :mswin then s.downcase
|
191
|
+
else s
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def self.formalize(s)
|
196
|
+
FORMALIZATIONS.each_pair do |pattern, result|
|
197
|
+
return result if pattern === s
|
198
|
+
end
|
199
|
+
return s
|
200
|
+
end
|
201
|
+
|
202
|
+
def self.program_message(message, options={})
|
203
|
+
prog_pattern = options[:prog] || /%prog\b/
|
204
|
+
|
205
|
+
steps = [$0]
|
206
|
+
steps << (program = self.canonize_path_case(File.basename(steps[-1])))
|
207
|
+
steps << (prog_name = self.formalize(File.basename(steps[-2], '.rb')))
|
208
|
+
steps << message.to_s
|
209
|
+
steps << steps[-1].gsub(prog_pattern, program)
|
210
|
+
steps << steps[-1].gsub(/%program_name\b/, prog_name)
|
211
|
+
steps << steps[-1].gsub(/%cmd\b/, options[:cmd] || '<cmd>')
|
212
|
+
return steps[-1]
|
213
|
+
rescue
|
214
|
+
STDERR.puts "steps: " + steps.inspect
|
215
|
+
raise
|
216
|
+
end
|
217
|
+
|
218
|
+
class SchemaError < Error; end
|
219
|
+
|
220
|
+
class AccessArtifact
|
221
|
+
def definition_sql
|
222
|
+
[
|
223
|
+
check_existence_sql(false, "%s existed before definition"),
|
224
|
+
creation_notice,
|
225
|
+
creation_sql + ";",
|
226
|
+
check_existence_sql(true, "%s was not created by definition"),
|
227
|
+
insert_access_creation_record_sql,
|
228
|
+
].compact.join(ddl_block_separator)
|
229
|
+
end
|
230
|
+
|
231
|
+
attr_accessor :file_path, :filename_metavariable
|
232
|
+
|
233
|
+
def ddl_block_separator
|
234
|
+
"\n"
|
235
|
+
end
|
236
|
+
|
237
|
+
def check_existence_sql(for_existence, error_message)
|
238
|
+
nil
|
239
|
+
end
|
240
|
+
|
241
|
+
def creation_notice
|
242
|
+
nil
|
243
|
+
end
|
244
|
+
|
245
|
+
def creation_sql
|
246
|
+
if metavar = filename_metavariable
|
247
|
+
@definition.gsub(metavar) {|m| self.name}
|
248
|
+
else
|
249
|
+
@definition
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def insert_access_creation_record_sql
|
254
|
+
nil
|
255
|
+
end
|
256
|
+
|
257
|
+
def printable_type
|
258
|
+
self.class.name.split('::').last.scan(/[A-Z]+[a-z]*/).collect {|p| p.downcase}.join(' ')
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
class StoredProcedure < AccessArtifact
|
263
|
+
OBJECT_TYPE = "PROCEDURE"
|
264
|
+
|
265
|
+
# Construct with a hash (as if loaded from a stored procedure YAML file)
|
266
|
+
def initialize(sproc_info)
|
267
|
+
@name = sproc_info["name"].dup.freeze
|
268
|
+
@definition = sproc_info["sql"].dup.freeze
|
269
|
+
end
|
270
|
+
|
271
|
+
attr_reader :name
|
272
|
+
|
273
|
+
def depends_on
|
274
|
+
[]
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
class View < AccessArtifact
|
279
|
+
OBJECT_TYPE = "VIEW"
|
280
|
+
|
281
|
+
# Construct with a hash (as if loaded from a view YAML file)
|
282
|
+
def initialize(view_info)
|
283
|
+
@name = view_info["name"].dup.freeze
|
284
|
+
@depends_on = view_info.fetch("referencing", []).dup.freeze
|
285
|
+
@definition = view_info["sql"].dup.freeze
|
286
|
+
end
|
287
|
+
|
288
|
+
attr_reader :name, :depends_on
|
289
|
+
end
|
290
|
+
|
291
|
+
class Function < AccessArtifact
|
292
|
+
OBJECT_TYPE = "FUNCTION"
|
293
|
+
|
294
|
+
# Construct with a hash (as if loaded from a function YAML file)
|
295
|
+
def initialize(func_info)
|
296
|
+
@name = func_info["name"].dup.freeze
|
297
|
+
@depends_on = func_info.fetch("referencing", []).dup.freeze
|
298
|
+
@definition = func_info["sql"].dup.freeze
|
299
|
+
end
|
300
|
+
|
301
|
+
attr_reader :name, :depends_on
|
302
|
+
end
|
303
|
+
|
304
|
+
class << self
|
305
|
+
def access_artifact(info)
|
306
|
+
case info["define"]
|
307
|
+
when "stored procedure" then StoredProcedure.new(info)
|
308
|
+
when "view" then View.new(info)
|
309
|
+
when "function" then Function.new(info)
|
310
|
+
else
|
311
|
+
raise SchemaError, "'define' not specified for access artifact '#{info['name']}'"
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
def load_access_artifact(path)
|
316
|
+
info = YAML.load_file(path)
|
317
|
+
info['name'] = File.basename(path, '.yaml')
|
318
|
+
artifact = access_artifact(info)
|
319
|
+
artifact.file_path = File.expand_path(path)
|
320
|
+
return artifact
|
321
|
+
end
|
322
|
+
|
323
|
+
def each_access_artifact(path)
|
324
|
+
Dir.glob(File.join(path, '*.yaml')).each do |fpath|
|
325
|
+
artifact = load_access_artifact(fpath)
|
326
|
+
(yield artifact) if artifact
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
def yaml_path(path)
|
331
|
+
path_s = path.to_s
|
332
|
+
if path_s.end_with?('.yaml')
|
333
|
+
return path
|
334
|
+
else
|
335
|
+
return path.class.new(path_s + '.yaml')
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
def secure_digest(s)
|
340
|
+
[Digest::MD5.digest(s)].pack('m0').chomp
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
class AccessArtifactCollection
|
345
|
+
def initialize(path, options={})
|
346
|
+
@items = Hash.new
|
347
|
+
db_specifics = options[:db_specifics]
|
348
|
+
filename_metavariable = options[:filename_metavariable]
|
349
|
+
filename_metavariable = filename_metavariable.dup.freeze if filename_metavariable
|
350
|
+
|
351
|
+
XMigra.each_access_artifact(path) do |artifact|
|
352
|
+
@items[artifact.name] = artifact
|
353
|
+
artifact.extend(db_specifics) if db_specifics
|
354
|
+
artifact.filename_metavariable = filename_metavariable
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
def [](name)
|
359
|
+
@items[name]
|
360
|
+
end
|
361
|
+
|
362
|
+
def names
|
363
|
+
@items.keys
|
364
|
+
end
|
365
|
+
|
366
|
+
def at_path(fpath)
|
367
|
+
fpath = File.expand_path(fpath)
|
368
|
+
return find {|i| i.file_path == fpath}
|
369
|
+
end
|
370
|
+
|
371
|
+
def each(&block); @items.each_value(&block); end
|
372
|
+
alias tsort_each_node each
|
373
|
+
|
374
|
+
def tsort_each_child(node)
|
375
|
+
node.depends_on.each do |child|
|
376
|
+
yield @items[child]
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
include Enumerable
|
381
|
+
include TSort
|
382
|
+
|
383
|
+
def each_definition_sql
|
384
|
+
tsort_each do |artifact|
|
385
|
+
yield artifact.definition_sql
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
class Index
|
391
|
+
def initialize(index_info)
|
392
|
+
@name = index_info['name'].dup.freeze
|
393
|
+
@definition = index_info['sql'].dup.freeze
|
394
|
+
end
|
395
|
+
|
396
|
+
attr_reader :name
|
397
|
+
|
398
|
+
attr_accessor :file_path
|
399
|
+
|
400
|
+
def id
|
401
|
+
XMigra.secure_digest(@definition)
|
402
|
+
end
|
403
|
+
|
404
|
+
def definition_sql
|
405
|
+
@definition
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
class IndexCollection
|
410
|
+
def initialize(path, options={})
|
411
|
+
@items = Hash.new
|
412
|
+
db_specifics = options[:db_specifics]
|
413
|
+
Dir.glob(File.join(path, '*.yaml')).each do |fpath|
|
414
|
+
info = YAML.load_file(fpath)
|
415
|
+
info['name'] = File.basename(fpath, '.yaml')
|
416
|
+
index = Index.new(info)
|
417
|
+
index.extend(db_specifics) if db_specifics
|
418
|
+
index.file_path = File.expand_path(fpath)
|
419
|
+
@items[index.name] = index
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
def [](name)
|
424
|
+
@items[name]
|
425
|
+
end
|
426
|
+
|
427
|
+
def names
|
428
|
+
@items.keys
|
429
|
+
end
|
430
|
+
|
431
|
+
def each(&block); @items.each_value(&block); end
|
432
|
+
include Enumerable
|
433
|
+
|
434
|
+
def each_definition_sql
|
435
|
+
each {|i| yield i.definition_sql}
|
436
|
+
end
|
437
|
+
|
438
|
+
def empty?
|
439
|
+
@items.empty?
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
class Migration
|
444
|
+
EMPTY_DB = 'empty database'
|
445
|
+
FOLLOWS = 'starting from'
|
446
|
+
CHANGES = 'changes'
|
447
|
+
|
448
|
+
def initialize(info)
|
449
|
+
@id = info['id'].dup.freeze
|
450
|
+
_follows = info[FOLLOWS]
|
451
|
+
@follows = (_follows.dup.freeze unless _follows == EMPTY_DB)
|
452
|
+
@sql = info["sql"].dup.freeze
|
453
|
+
@description = info["description"].dup.freeze
|
454
|
+
@changes = (info[CHANGES] || []).dup.freeze
|
455
|
+
@changes.each {|c| c.freeze}
|
456
|
+
end
|
457
|
+
|
458
|
+
attr_reader :id, :follows, :sql, :description, :changes
|
459
|
+
attr_accessor :file_path
|
460
|
+
|
461
|
+
class << self
|
462
|
+
def id_from_filename(fname)
|
463
|
+
XMigra.secure_digest(fname.upcase) # Base64 encoded digest
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
class MigrationChain < Array
|
469
|
+
HEAD_FILE = 'head.yaml'
|
470
|
+
LATEST_CHANGE = 'latest change'
|
471
|
+
MIGRATION_FILE_PATTERN = /^\d{4}-\d\d-\d\d.*\.yaml$/i
|
472
|
+
|
473
|
+
def initialize(path, options={})
|
474
|
+
super()
|
475
|
+
|
476
|
+
db_specifics = options[:db_specifics]
|
477
|
+
vcs_specifics = options[:vcs_specifics]
|
478
|
+
|
479
|
+
head_info = YAML.load_file(File.join(path, HEAD_FILE))
|
480
|
+
file = head_info[LATEST_CHANGE]
|
481
|
+
prev_file = HEAD_FILE
|
482
|
+
files_loaded = []
|
483
|
+
|
484
|
+
until file.nil?
|
485
|
+
file = XMigra.yaml_path(file)
|
486
|
+
fpath = File.join(path, file)
|
487
|
+
break unless File.file?(fpath)
|
488
|
+
begin
|
489
|
+
mig_info = YAML.load_file(fpath)
|
490
|
+
rescue
|
491
|
+
raise XMigra::Error, "Error loading/parsing #{fpath}"
|
492
|
+
end
|
493
|
+
files_loaded << file
|
494
|
+
mig_info["id"] = Migration::id_from_filename(file)
|
495
|
+
migration = Migration.new(mig_info)
|
496
|
+
migration.file_path = File.expand_path(fpath)
|
497
|
+
migration.extend(db_specifics) if db_specifics
|
498
|
+
migration.extend(vcs_specifics) if vcs_specifics
|
499
|
+
unshift(migration)
|
500
|
+
prev_file = file
|
501
|
+
file = migration.follows
|
502
|
+
unless file.nil? || MIGRATION_FILE_PATTERN.match(XMigra.yaml_path(file))
|
503
|
+
raise XMigra::Error, "Invalid migration file \"#{file}\" referenced from \"#{prev_file}\""
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
@other_migrations = []
|
508
|
+
Dir.foreach(path) do |fname|
|
509
|
+
if MIGRATION_FILE_PATTERN.match(fname) && !files_loaded.include?(fname)
|
510
|
+
@other_migrations << fname.freeze
|
511
|
+
end
|
512
|
+
end
|
513
|
+
@other_migrations.freeze
|
514
|
+
end
|
515
|
+
|
516
|
+
# Test if the chain reaches back to the empty database
|
517
|
+
def complete?
|
518
|
+
length > 0 && self[0].follows.nil?
|
519
|
+
end
|
520
|
+
|
521
|
+
# Test if the chain encompasses all migration-like filenames in the path
|
522
|
+
def includes_all?
|
523
|
+
@other_migrations.empty?
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
527
|
+
class MigrationConflict
|
528
|
+
def initialize(path, branch_point, heads)
|
529
|
+
@path = Pathname.new(path)
|
530
|
+
@branch_point = branch_point
|
531
|
+
@heads = heads
|
532
|
+
@branch_use = :undefined
|
533
|
+
@scope = :repository
|
534
|
+
@after_fix = nil
|
535
|
+
end
|
536
|
+
|
537
|
+
attr_accessor :branch_use, :scope, :after_fix
|
538
|
+
|
539
|
+
def resolvable?
|
540
|
+
head_0 = @heads[0]
|
541
|
+
@heads[1].each_pair do |k, v|
|
542
|
+
next unless head_0.has_key?(k)
|
543
|
+
next if k == MigrationChain::LATEST_CHANGE
|
544
|
+
return false unless head_0[k] == v
|
545
|
+
end
|
546
|
+
|
547
|
+
return true
|
548
|
+
end
|
549
|
+
|
550
|
+
def migration_tweak
|
551
|
+
unless defined? @migration_to_fix and defined? @fixed_migration_contents
|
552
|
+
# Walk the chain from @head[1][MigrationChain::LATEST_CHANGE] and find
|
553
|
+
# the first migration after @branch_point
|
554
|
+
branch_file = XMigra.yaml_path(@branch_point)
|
555
|
+
cur_mig = XMigra.yaml_path(@heads[1][MigrationChain::LATEST_CHANGE])
|
556
|
+
until cur_mig.nil?
|
557
|
+
mig_info = YAML.load_file(@path.join(cur_mig))
|
558
|
+
prev_mig = XMigra.yaml_path(mig_info[Migration::FOLLOWS])
|
559
|
+
break if prev_mig == branch_file
|
560
|
+
cur_mig = prev_mig
|
561
|
+
end
|
562
|
+
|
563
|
+
mig_info[Migration::FOLLOWS] = @heads[0][MigrationChain::LATEST_CHANGE]
|
564
|
+
@migration_to_fix = cur_mig
|
565
|
+
@fixed_migration_contents = mig_info
|
566
|
+
end
|
567
|
+
|
568
|
+
return @migration_to_fix, @fixed_migration_contents
|
569
|
+
end
|
570
|
+
|
571
|
+
def fix_conflict!
|
572
|
+
raise(VersionControlError, "Unresolvable conflict") unless resolvable?
|
573
|
+
|
574
|
+
file_to_fix, fixed_contents = migration_tweak
|
575
|
+
|
576
|
+
# Rewrite the head file
|
577
|
+
head_info = @heads[0].merge(@heads[1]) # This means @heads[1]'s LATEST_CHANGE wins
|
578
|
+
File.open(@path.join(MigrationChain::HEAD_FILE), 'w') do |f|
|
579
|
+
$xmigra_yamler.dump(head_info, f)
|
580
|
+
end
|
581
|
+
|
582
|
+
# Rewrite the first migration (on the current branch) after @branch_point
|
583
|
+
File.open(@path.join(file_to_fix), 'w') do |f|
|
584
|
+
$xmigra_yamler.dump(fixed_contents, f)
|
585
|
+
end
|
586
|
+
|
587
|
+
if @after_fix
|
588
|
+
@after_fix.call
|
589
|
+
end
|
590
|
+
end
|
591
|
+
end
|
592
|
+
|
593
|
+
class BranchUpgrade
|
594
|
+
TARGET_BRANCH = "resulting branch"
|
595
|
+
MIGRATION_COMPLETED = "completes migration to"
|
596
|
+
|
597
|
+
def initialize(path)
|
598
|
+
@file_path = path
|
599
|
+
@warnings = []
|
600
|
+
|
601
|
+
verinc_info = {}
|
602
|
+
if path.exist?
|
603
|
+
@found = true
|
604
|
+
begin
|
605
|
+
verinc_info = YAML.load_file(path)
|
606
|
+
rescue Error => e
|
607
|
+
warning "Failed to load branch upgrade migration (#{e.class}).\n #{e}"
|
608
|
+
verinc_info = {}
|
609
|
+
end
|
610
|
+
end
|
611
|
+
|
612
|
+
@base_migration = verinc_info[Migration::FOLLOWS]
|
613
|
+
@target_branch = (XMigra.secure_digest(verinc_info[TARGET_BRANCH]) if verinc_info.has_key? TARGET_BRANCH)
|
614
|
+
@migration_completed = verinc_info[MIGRATION_COMPLETED]
|
615
|
+
@sql = verinc_info['sql']
|
616
|
+
end
|
617
|
+
|
618
|
+
attr_reader :file_path, :base_migration, :target_branch, :migration_completed, :sql
|
619
|
+
|
620
|
+
def found?
|
621
|
+
@found
|
622
|
+
end
|
623
|
+
|
624
|
+
def applicable?(mig_chain)
|
625
|
+
return false if mig_chain.length < 1
|
626
|
+
return false unless (@base_migration && @target_branch)
|
627
|
+
|
628
|
+
return File.basename(mig_chain[-1].file_path) == XMigra.yaml_path(@base_migration)
|
629
|
+
end
|
630
|
+
|
631
|
+
def has_warnings?
|
632
|
+
not @warnings.empty?
|
633
|
+
end
|
634
|
+
|
635
|
+
def warnings
|
636
|
+
@warnings.dup
|
637
|
+
end
|
638
|
+
|
639
|
+
def migration_completed_id
|
640
|
+
Migration.id_from_filename(XMigra.yaml_path(migration_completed))
|
641
|
+
end
|
642
|
+
|
643
|
+
private
|
644
|
+
|
645
|
+
def warning(s)
|
646
|
+
s.freeze
|
647
|
+
@warnings << s
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
module NoSpecifics; end
|
652
|
+
|
653
|
+
module MSSQLSpecifics
|
654
|
+
IDENTIFIER_SUBPATTERN = '[a-z_@#][a-z0-9@$#_]*|"[^\[\]"]+"|\[[^\[\]]+\]'
|
655
|
+
DBNAME_PATTERN = /^
|
656
|
+
(?:(#{IDENTIFIER_SUBPATTERN})\.)?
|
657
|
+
(#{IDENTIFIER_SUBPATTERN})
|
658
|
+
$/ix
|
659
|
+
STATISTICS_FILE = 'statistics-objects.yaml'
|
660
|
+
|
661
|
+
class StatisticsObject
|
662
|
+
def initialize(name, params)
|
663
|
+
(@name = name.dup).freeze
|
664
|
+
(@target = params[0].dup).freeze
|
665
|
+
(@columns = params[1].dup).freeze
|
666
|
+
@options = params[2] || {}
|
667
|
+
@options.freeze
|
668
|
+
@options.each_value {|v| v.freeze}
|
669
|
+
end
|
670
|
+
|
671
|
+
attr_reader :name, :target, :columns, :options
|
672
|
+
|
673
|
+
def creation_sql
|
674
|
+
result = "CREATE STATISTICS #{name} ON #{target} (#{columns})"
|
675
|
+
|
676
|
+
result += " WHERE " + @options['where'] if @options['where']
|
677
|
+
result += " WITH " + @options['with'] if @options['with']
|
678
|
+
|
679
|
+
result += ";"
|
680
|
+
return result
|
681
|
+
end
|
682
|
+
end
|
683
|
+
|
684
|
+
def ddl_block_separator; "\nGO\n"; end
|
685
|
+
def filename_metavariable; "[{filename}]"; end
|
686
|
+
|
687
|
+
def stats_objs
|
688
|
+
return @stats_objs if @stats_objs
|
689
|
+
|
690
|
+
begin
|
691
|
+
stats_data = YAML::load_file(path.join(MSSQLSpecifics::STATISTICS_FILE))
|
692
|
+
rescue Errno::ENOENT
|
693
|
+
return @stats_objs = [].freeze
|
694
|
+
end
|
695
|
+
|
696
|
+
@stats_objs = stats_data.collect(&StatisticsObject.method(:new))
|
697
|
+
@stats_objs.each {|o| o.freeze}
|
698
|
+
@stats_objs.freeze
|
699
|
+
|
700
|
+
return @stats_objs
|
701
|
+
end
|
702
|
+
|
703
|
+
def in_ddl_transaction
|
704
|
+
parts = []
|
705
|
+
parts << <<-"END_OF_SQL"
|
706
|
+
SET ANSI_NULLS ON
|
707
|
+
GO
|
708
|
+
|
709
|
+
SET QUOTED_IDENTIFIER ON
|
710
|
+
GO
|
711
|
+
|
712
|
+
SET ANSI_PADDING ON
|
713
|
+
GO
|
714
|
+
|
715
|
+
SET NOCOUNT ON
|
716
|
+
GO
|
717
|
+
|
718
|
+
BEGIN TRY
|
719
|
+
BEGIN TRAN;
|
720
|
+
END_OF_SQL
|
721
|
+
|
722
|
+
each_batch(yield) do |batch|
|
723
|
+
batch_literal = MSSQLSpecifics.string_literal("\n" + batch)
|
724
|
+
parts << "EXEC sp_executesql @statement = #{batch_literal};"
|
725
|
+
end
|
726
|
+
|
727
|
+
parts << <<-"END_OF_SQL"
|
728
|
+
COMMIT TRAN;
|
729
|
+
END TRY
|
730
|
+
BEGIN CATCH
|
731
|
+
ROLLBACK TRAN;
|
732
|
+
|
733
|
+
DECLARE @ErrorMessage NVARCHAR(4000);
|
734
|
+
DECLARE @ErrorSeverity INT;
|
735
|
+
DECLARE @ErrorState INT;
|
736
|
+
|
737
|
+
PRINT N'Update failed: ' + ERROR_MESSAGE();
|
738
|
+
PRINT N' State: ' + CAST(ERROR_STATE() AS NVARCHAR);
|
739
|
+
PRINT N' Line: ' + CAST(ERROR_LINE() AS NVARCHAR);
|
740
|
+
|
741
|
+
SELECT
|
742
|
+
@ErrorMessage = N'Update failed: ' + ERROR_MESSAGE(),
|
743
|
+
@ErrorSeverity = ERROR_SEVERITY(),
|
744
|
+
@ErrorState = ERROR_STATE();
|
745
|
+
|
746
|
+
-- Use RAISERROR inside the CATCH block to return error
|
747
|
+
-- information about the original error that caused
|
748
|
+
-- execution to jump to the CATCH block.
|
749
|
+
RAISERROR (@ErrorMessage, -- Message text.
|
750
|
+
@ErrorSeverity, -- Severity.
|
751
|
+
@ErrorState -- State.
|
752
|
+
);
|
753
|
+
END CATCH;
|
754
|
+
END_OF_SQL
|
755
|
+
|
756
|
+
return parts.join("\n")
|
757
|
+
end
|
758
|
+
|
759
|
+
def amend_script_parts(parts)
|
760
|
+
parts.insert_after(
|
761
|
+
:create_and_fill_indexes_table_sql,
|
762
|
+
:create_and_fill_statistics_table_sql
|
763
|
+
)
|
764
|
+
parts.insert_after(
|
765
|
+
:remove_undesired_indexes_sql,
|
766
|
+
:remove_undesired_statistics_sql
|
767
|
+
)
|
768
|
+
parts.insert_after(:create_new_indexes_sql, :create_new_statistics_sql)
|
769
|
+
end
|
770
|
+
|
771
|
+
def check_execution_environment_sql
|
772
|
+
<<-"END_OF_SQL"
|
773
|
+
PRINT N'Checking execution environment:';
|
774
|
+
IF DB_NAME() IN ('master', 'tempdb', 'model', 'msdb')
|
775
|
+
BEGIN
|
776
|
+
RAISERROR(N'Please select an appropriate target database for the update script.', 11, 1);
|
777
|
+
END;
|
778
|
+
END_OF_SQL
|
779
|
+
end
|
780
|
+
|
781
|
+
def ensure_version_tables_sql
|
782
|
+
<<-"END_OF_SQL"
|
783
|
+
PRINT N'Ensuring version tables:';
|
784
|
+
IF NOT EXISTS (
|
785
|
+
SELECT * FROM sys.schemas
|
786
|
+
WHERE name = N'xmigra'
|
787
|
+
)
|
788
|
+
BEGIN
|
789
|
+
EXEC sp_executesql N'
|
790
|
+
CREATE SCHEMA [xmigra] AUTHORIZATION [dbo];
|
791
|
+
';
|
792
|
+
END;
|
793
|
+
GO
|
794
|
+
|
795
|
+
IF NOT EXISTS (
|
796
|
+
SELECT * FROM sys.objects
|
797
|
+
WHERE object_id = OBJECT_ID(N'[xmigra].[applied]')
|
798
|
+
AND type IN (N'U')
|
799
|
+
)
|
800
|
+
BEGIN
|
801
|
+
CREATE TABLE [xmigra].[applied] (
|
802
|
+
[MigrationID] nvarchar(80) NOT NULL,
|
803
|
+
[ApplicationOrder] int IDENTITY(1,1) NOT NULL,
|
804
|
+
[VersionBridgeMark] bit NOT NULL,
|
805
|
+
[Description] nvarchar(max) NOT NULL,
|
806
|
+
|
807
|
+
CONSTRAINT [PK_version] PRIMARY KEY CLUSTERED (
|
808
|
+
[MigrationID] ASC
|
809
|
+
) WITH (
|
810
|
+
PAD_INDEX = OFF,
|
811
|
+
STATISTICS_NORECOMPUTE = OFF,
|
812
|
+
IGNORE_DUP_KEY = OFF,
|
813
|
+
ALLOW_ROW_LOCKS = ON,
|
814
|
+
ALLOW_PAGE_LOCKS = ON
|
815
|
+
) ON [PRIMARY]
|
816
|
+
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY];
|
817
|
+
END;
|
818
|
+
GO
|
819
|
+
|
820
|
+
IF NOT EXISTS (
|
821
|
+
SELECT * FROM sys.objects
|
822
|
+
WHERE object_id = OBJECT_ID(N'[xmigra].[DF_version_VersionBridgeMark]')
|
823
|
+
AND type IN (N'D')
|
824
|
+
)
|
825
|
+
BEGIN
|
826
|
+
ALTER TABLE [xmigra].[applied] ADD CONSTRAINT [DF_version_VersionBridgeMark]
|
827
|
+
DEFAULT (0) FOR [VersionBridgeMark];
|
828
|
+
END;
|
829
|
+
GO
|
830
|
+
|
831
|
+
IF NOT EXISTS (
|
832
|
+
SELECT * FROM sys.objects
|
833
|
+
WHERE object_id = OBJECT_ID(N'[xmigra].[access_objects]')
|
834
|
+
AND type IN (N'U')
|
835
|
+
)
|
836
|
+
BEGIN
|
837
|
+
CREATE TABLE [xmigra].[access_objects] (
|
838
|
+
[type] nvarchar(40) NOT NULL,
|
839
|
+
[name] nvarchar(256) NOT NULL,
|
840
|
+
[order] int identity(1,1) NOT NULL,
|
841
|
+
|
842
|
+
CONSTRAINT [PK_access_objects] PRIMARY KEY CLUSTERED (
|
843
|
+
[name] ASC
|
844
|
+
) WITH (
|
845
|
+
PAD_INDEX = OFF,
|
846
|
+
STATISTICS_NORECOMPUTE = OFF,
|
847
|
+
IGNORE_DUP_KEY = OFF,
|
848
|
+
ALLOW_ROW_LOCKS = ON,
|
849
|
+
ALLOW_PAGE_LOCKS = ON
|
850
|
+
) ON [PRIMARY]
|
851
|
+
) ON [PRIMARY];
|
852
|
+
END;
|
853
|
+
GO
|
854
|
+
|
855
|
+
IF NOT EXISTS (
|
856
|
+
SELECT * FROM sys.objects
|
857
|
+
WHERE object_id = OBJECT_ID(N'[xmigra].[indexes]')
|
858
|
+
AND type in (N'U')
|
859
|
+
)
|
860
|
+
BEGIN
|
861
|
+
CREATE TABLE [xmigra].[indexes] (
|
862
|
+
[IndexID] nvarchar(80) NOT NULL PRIMARY KEY,
|
863
|
+
[name] nvarchar(256) NOT NULL
|
864
|
+
) ON [PRIMARY];
|
865
|
+
END;
|
866
|
+
|
867
|
+
IF NOT EXISTS (
|
868
|
+
SELECT * FROM sys.objects
|
869
|
+
WHERE object_id = OBJECT_ID(N'[xmigra].[statistics]')
|
870
|
+
AND type in (N'U')
|
871
|
+
)
|
872
|
+
BEGIN
|
873
|
+
CREATE TABLE [xmigra].[statistics] (
|
874
|
+
[Name] nvarchar(100) NOT NULL PRIMARY KEY,
|
875
|
+
[Columns] nvarchar(256) NOT NULL
|
876
|
+
) ON [PRIMARY];
|
877
|
+
END;
|
878
|
+
|
879
|
+
IF NOT EXISTS (
|
880
|
+
SELECT * FROM sys.objects
|
881
|
+
WHERE object_id = OBJECT_ID(N'[xmigra].[branch_upgrade]')
|
882
|
+
AND type in (N'U')
|
883
|
+
)
|
884
|
+
BEGIN
|
885
|
+
CREATE TABLE [xmigra].[branch_upgrade] (
|
886
|
+
[ApplicationOrder] int identity(1,1) NOT NULL,
|
887
|
+
[Current] nvarchar(80) NOT NULL PRIMARY KEY,
|
888
|
+
[Next] nvarchar(80) NULL,
|
889
|
+
[UpgradeSql] nvarchar(max) NULL,
|
890
|
+
[CompletesMigration] nvarchar(80) NULL
|
891
|
+
) ON [PRIMARY];
|
892
|
+
END;
|
893
|
+
END_OF_SQL
|
894
|
+
end
|
895
|
+
|
896
|
+
def create_and_fill_migration_table_sql
|
897
|
+
intro = <<-"END_OF_SQL"
|
898
|
+
IF EXISTS (
|
899
|
+
SELECT * FROM sys.objects
|
900
|
+
WHERE object_id = OBJECT_ID(N'[xmigra].[migrations]')
|
901
|
+
AND type IN (N'U')
|
902
|
+
)
|
903
|
+
BEGIN
|
904
|
+
DROP TABLE [xmigra].[migrations];
|
905
|
+
END;
|
906
|
+
GO
|
907
|
+
|
908
|
+
CREATE TABLE [xmigra].[migrations] (
|
909
|
+
[MigrationID] nvarchar(80) NOT NULL,
|
910
|
+
[ApplicationOrder] int NOT NULL,
|
911
|
+
[Description] ntext NOT NULL,
|
912
|
+
[Install] bit NOT NULL DEFAULT(0)
|
913
|
+
);
|
914
|
+
GO
|
915
|
+
|
916
|
+
END_OF_SQL
|
917
|
+
|
918
|
+
mig_insert = <<-"END_OF_SQL"
|
919
|
+
INSERT INTO [xmigra].[migrations] (
|
920
|
+
[MigrationID],
|
921
|
+
[ApplicationOrder],
|
922
|
+
[Description]
|
923
|
+
) VALUES
|
924
|
+
END_OF_SQL
|
925
|
+
|
926
|
+
if (@db_info || {}).fetch('MSSQL 2005 compatible', false).eql?(true)
|
927
|
+
parts = [intro]
|
928
|
+
(0...migrations.length).each do |i|
|
929
|
+
m = migrations[i]
|
930
|
+
description_literal = MSSQLSpecifics.string_literal(m.description.strip)
|
931
|
+
parts << mig_insert + "(N'#{m.id}', #{i + 1}, #{description_literal});\n"
|
932
|
+
end
|
933
|
+
return parts.join('')
|
934
|
+
else
|
935
|
+
return intro + mig_insert + (0...migrations.length).collect do |i|
|
936
|
+
m = migrations[i]
|
937
|
+
description_literal = MSSQLSpecifics.string_literal(m.description.strip)
|
938
|
+
"(N'#{m.id}', #{i + 1}, #{description_literal})"
|
939
|
+
end.join(",\n") + ";\n"
|
940
|
+
end
|
941
|
+
end
|
942
|
+
|
943
|
+
def create_and_fill_indexes_table_sql
|
944
|
+
intro = <<-"END_OF_SQL"
|
945
|
+
PRINT N'Creating and filling index manipulation table:';
|
946
|
+
IF EXISTS (
|
947
|
+
SELECT * FROM sys.objects
|
948
|
+
WHERE object_id = OBJECT_ID(N'[xmigra].[updated_indexes]')
|
949
|
+
AND type IN (N'U')
|
950
|
+
)
|
951
|
+
BEGIN
|
952
|
+
DROP TABLE [xmigra].[updated_indexes];
|
953
|
+
END;
|
954
|
+
GO
|
955
|
+
|
956
|
+
CREATE TABLE [xmigra].[updated_indexes] (
|
957
|
+
[IndexID] NVARCHAR(80) NOT NULL PRIMARY KEY
|
958
|
+
);
|
959
|
+
GO
|
960
|
+
|
961
|
+
END_OF_SQL
|
962
|
+
|
963
|
+
insertion = <<-"END_OF_SQL"
|
964
|
+
INSERT INTO [xmigra].[updated_indexes] ([IndexID]) VALUES
|
965
|
+
END_OF_SQL
|
966
|
+
|
967
|
+
strlit = MSSQLSpecifics.method :string_literal
|
968
|
+
return intro + insertion + indexes.collect do |index|
|
969
|
+
"(#{strlit[index.id]})"
|
970
|
+
end.join(",\n") + ";\n" unless indexes.empty?
|
971
|
+
|
972
|
+
return intro
|
973
|
+
end
|
974
|
+
|
975
|
+
def create_and_fill_statistics_table_sql
|
976
|
+
intro = <<-"END_OF_SQL"
|
977
|
+
PRINT N'Creating and filling statistics object manipulation table:';
|
978
|
+
IF EXISTS (
|
979
|
+
SELECT * FROM sys.objects
|
980
|
+
WHERE object_id = OBJECT_ID(N'[xmigra].[updated_statistics]')
|
981
|
+
AND type in (N'U')
|
982
|
+
)
|
983
|
+
BEGIN
|
984
|
+
DROP TABLE [xmigra].[updated_statistics];
|
985
|
+
END;
|
986
|
+
GO
|
987
|
+
|
988
|
+
CREATE TABLE [xmigra].[updated_statistics] (
|
989
|
+
[Name] nvarchar(100) NOT NULL PRIMARY KEY,
|
990
|
+
[Columns] nvarchar(256) NOT NULL
|
991
|
+
);
|
992
|
+
GO
|
993
|
+
|
994
|
+
END_OF_SQL
|
995
|
+
|
996
|
+
insertion = <<-"END_OF_SQL"
|
997
|
+
INSERT INTO [xmigra].[updated_statistics] ([Name], [Columns]) VALUES
|
998
|
+
END_OF_SQL
|
999
|
+
|
1000
|
+
strlit = MSSQLSpecifics.method :string_literal
|
1001
|
+
return intro + insertion + stats_objs.collect do |stats_obj|
|
1002
|
+
"(#{strlit[stats_obj.name]}, #{strlit[stats_obj.columns]})"
|
1003
|
+
end.join(",\n") + ";\n" unless stats_objs.empty?
|
1004
|
+
end
|
1005
|
+
|
1006
|
+
def check_preceding_migrations_sql
|
1007
|
+
parts = []
|
1008
|
+
|
1009
|
+
parts << (<<-"END_OF_SQL") if production
|
1010
|
+
IF EXISTS (
|
1011
|
+
SELECT TOP(1) * FROM [xmigra].[branch_upgrade]
|
1012
|
+
) AND NOT EXISTS (
|
1013
|
+
SELECT TOP(1) * FROM [xmigra].[branch_upgrade]
|
1014
|
+
WHERE #{branch_id_literal} IN ([Current], [Next])
|
1015
|
+
)
|
1016
|
+
RAISERROR (N'Existing database is from a different (and non-upgradable) branch.', 11, 1);
|
1017
|
+
|
1018
|
+
END_OF_SQL
|
1019
|
+
|
1020
|
+
parts << (<<-"END_OF_SQL")
|
1021
|
+
IF NOT #{upgrading_to_new_branch_test_sql}
|
1022
|
+
BEGIN
|
1023
|
+
PRINT N'Checking preceding migrations:';
|
1024
|
+
-- Get the ApplicationOrder of the most recent version bridge migration
|
1025
|
+
DECLARE @VersionBridge INT;
|
1026
|
+
SET @VersionBridge = (
|
1027
|
+
SELECT COALESCE(MAX([ApplicationOrder]), 0)
|
1028
|
+
FROM [xmigra].[applied]
|
1029
|
+
WHERE [VersionBridgeMark] <> 0
|
1030
|
+
);
|
1031
|
+
|
1032
|
+
-- Check for existence of applied migrations after the latest version
|
1033
|
+
-- bridge that are not in [xmigra].[migrations]
|
1034
|
+
IF EXISTS (
|
1035
|
+
SELECT * FROM [xmigra].[applied] a
|
1036
|
+
WHERE a.[ApplicationOrder] > @VersionBridge
|
1037
|
+
AND a.[MigrationID] NOT IN (
|
1038
|
+
SELECT m.[MigrationID] FROM [xmigra].[migrations] m
|
1039
|
+
)
|
1040
|
+
)
|
1041
|
+
RAISERROR (N'Unknown in-version migrations have been applied.', 11, 1);
|
1042
|
+
END;
|
1043
|
+
END_OF_SQL
|
1044
|
+
|
1045
|
+
return parts.join('')
|
1046
|
+
end
|
1047
|
+
|
1048
|
+
def check_chain_continuity_sql
|
1049
|
+
<<-"END_OF_SQL"
|
1050
|
+
IF NOT #{upgrading_to_new_branch_test_sql}
|
1051
|
+
BEGIN
|
1052
|
+
PRINT N'Checking migration chain continuity:';
|
1053
|
+
-- Get the [xmigra].[migrations] ApplicationOrder of the most recent version bridge migration
|
1054
|
+
DECLARE @BridgePoint INT;
|
1055
|
+
SET @BridgePoint = (
|
1056
|
+
SELECT COALESCE(MAX(m.[ApplicationOrder]), 0)
|
1057
|
+
FROM [xmigra].[applied] a
|
1058
|
+
INNER JOIN [xmigra].[migrations] m
|
1059
|
+
ON a.[MigrationID] = m.[MigrationID]
|
1060
|
+
WHERE a.[VersionBridgeMark] <> 0
|
1061
|
+
);
|
1062
|
+
|
1063
|
+
-- Test for previously applied migrations that break the continuity of the
|
1064
|
+
-- migration chain in this script:
|
1065
|
+
IF EXISTS (
|
1066
|
+
SELECT *
|
1067
|
+
FROM [xmigra].[applied] a
|
1068
|
+
INNER JOIN [xmigra].[migrations] m
|
1069
|
+
ON a.[MigrationID] = m.[MigrationID]
|
1070
|
+
INNER JOIN [xmigra].[migrations] p
|
1071
|
+
ON m.[ApplicationOrder] - 1 = p.[ApplicationOrder]
|
1072
|
+
WHERE p.[ApplicationOrder] > @BridgePoint
|
1073
|
+
AND p.[MigrationID] NOT IN (
|
1074
|
+
SELECT a2.[MigrationID] FROM [xmigra].[applied] a2
|
1075
|
+
)
|
1076
|
+
)
|
1077
|
+
BEGIN
|
1078
|
+
RAISERROR(
|
1079
|
+
N'Previously applied migrations interrupt the continuity of the migration chain',
|
1080
|
+
11,
|
1081
|
+
1
|
1082
|
+
);
|
1083
|
+
END;
|
1084
|
+
END;
|
1085
|
+
END_OF_SQL
|
1086
|
+
end
|
1087
|
+
|
1088
|
+
def select_for_install_sql
|
1089
|
+
<<-"END_OF_SQL"
|
1090
|
+
PRINT N'Selecting migrations to apply:';
|
1091
|
+
DECLARE @BridgePoint INT;
|
1092
|
+
IF #{upgrading_to_new_branch_test_sql}
|
1093
|
+
BEGIN
|
1094
|
+
-- Get the [xmigra].[migrations] ApplicationOrder of the record corresponding to the branch transition
|
1095
|
+
SET @BridgePoint = (
|
1096
|
+
SELECT MAX(m.[ApplicationOrder])
|
1097
|
+
FROM [xmigra].[migrations] m
|
1098
|
+
INNER JOIN [xmigra].[branch_upgrade] bu
|
1099
|
+
ON m.[MigrationID] = bu.[CompletesMigration]
|
1100
|
+
);
|
1101
|
+
|
1102
|
+
UPDATE [xmigra].[migrations]
|
1103
|
+
SET [Install] = 1
|
1104
|
+
WHERE [ApplicationOrder] > @BridgePoint;
|
1105
|
+
END
|
1106
|
+
ELSE BEGIN
|
1107
|
+
-- Get the [xmigra].[migrations] ApplicationOrder of the most recent version bridge migration
|
1108
|
+
SET @BridgePoint = (
|
1109
|
+
SELECT COALESCE(MAX(m.[ApplicationOrder]), 0)
|
1110
|
+
FROM [xmigra].[applied] a
|
1111
|
+
INNER JOIN [xmigra].[migrations] m
|
1112
|
+
ON a.[MigrationID] = m.[MigrationID]
|
1113
|
+
WHERE a.[VersionBridgeMark] <> 0
|
1114
|
+
);
|
1115
|
+
|
1116
|
+
UPDATE [xmigra].[migrations]
|
1117
|
+
SET [Install] = 1
|
1118
|
+
WHERE [MigrationID] NOT IN (
|
1119
|
+
SELECT a.[MigrationID] FROM [xmigra].[applied] a
|
1120
|
+
)
|
1121
|
+
AND [ApplicationOrder] > @BridgePoint;
|
1122
|
+
END;
|
1123
|
+
END_OF_SQL
|
1124
|
+
end
|
1125
|
+
|
1126
|
+
def production_config_check_sql
|
1127
|
+
unless production
|
1128
|
+
id_literal = MSSQLSpecifics.string_literal(@migrations[0].id)
|
1129
|
+
<<-"END_OF_SQL"
|
1130
|
+
PRINT N'Checking for production status:';
|
1131
|
+
IF EXISTS (
|
1132
|
+
SELECT * FROM [xmigra].[migrations]
|
1133
|
+
WHERE [MigrationID] = #{id_literal}
|
1134
|
+
AND [Install] <> 0
|
1135
|
+
)
|
1136
|
+
BEGIN
|
1137
|
+
CREATE TABLE [xmigra].[development] (
|
1138
|
+
[info] nvarchar(200) NOT NULL PRIMARY KEY
|
1139
|
+
);
|
1140
|
+
END;
|
1141
|
+
GO
|
1142
|
+
|
1143
|
+
IF NOT EXISTS (
|
1144
|
+
SELECT * FROM [sys].[objects]
|
1145
|
+
WHERE object_id = OBJECT_ID(N'[xmigra].[development]')
|
1146
|
+
AND type = N'U'
|
1147
|
+
)
|
1148
|
+
RAISERROR(N'Development script cannot be applied to a production database.', 11, 1);
|
1149
|
+
END_OF_SQL
|
1150
|
+
end
|
1151
|
+
end
|
1152
|
+
|
1153
|
+
def remove_access_artifacts_sql
|
1154
|
+
# Iterate the [xmigra].[access_objects] table and drop all access
|
1155
|
+
# objects previously created by xmigra
|
1156
|
+
return <<-"END_OF_SQL"
|
1157
|
+
PRINT N'Removing data access artifacts:';
|
1158
|
+
DECLARE @sqlcmd NVARCHAR(1000); -- Built SQL command
|
1159
|
+
DECLARE @obj_name NVARCHAR(256); -- Name of object to drop
|
1160
|
+
DECLARE @obj_type NVARCHAR(40); -- Type of object to drop
|
1161
|
+
|
1162
|
+
DECLARE AccObjs_cursor CURSOR LOCAL FOR
|
1163
|
+
SELECT [name], [type]
|
1164
|
+
FROM [xmigra].[access_objects]
|
1165
|
+
ORDER BY [order] DESC;
|
1166
|
+
|
1167
|
+
OPEN AccObjs_cursor;
|
1168
|
+
|
1169
|
+
FETCH NEXT FROM AccObjs_cursor INTO @obj_name, @obj_type;
|
1170
|
+
|
1171
|
+
WHILE @@FETCH_STATUS = 0 BEGIN
|
1172
|
+
SET @sqlcmd = N'DROP ' + @obj_type + N' ' + @obj_name + N';';
|
1173
|
+
EXEC sp_executesql @sqlcmd;
|
1174
|
+
|
1175
|
+
FETCH NEXT FROM AccObjs_cursor INTO @obj_name, @obj_type;
|
1176
|
+
END;
|
1177
|
+
|
1178
|
+
CLOSE AccObjs_cursor;
|
1179
|
+
DEALLOCATE AccObjs_cursor;
|
1180
|
+
|
1181
|
+
DELETE FROM [xmigra].[access_objects];
|
1182
|
+
|
1183
|
+
END_OF_SQL
|
1184
|
+
end
|
1185
|
+
|
1186
|
+
def remove_undesired_indexes_sql
|
1187
|
+
<<-"END_OF_SQL"
|
1188
|
+
PRINT N'Removing undesired indexes:';
|
1189
|
+
-- Iterate over indexes in [xmigra].[indexes] that don't have an entry in
|
1190
|
+
-- [xmigra].[updated_indexes].
|
1191
|
+
DECLARE @sqlcmd NVARCHAR(1000); -- Built SQL command
|
1192
|
+
DECLARE @index_name NVARCHAR(256); -- Name of index to drop
|
1193
|
+
DECLARE @table_name SYSNAME; -- Name of table owning index
|
1194
|
+
DECLARE @match_count INT; -- Number of matching index names
|
1195
|
+
|
1196
|
+
DECLARE Index_cursor CURSOR LOCAL FOR
|
1197
|
+
SELECT
|
1198
|
+
xi.[name],
|
1199
|
+
MAX(QUOTENAME(OBJECT_SCHEMA_NAME(si.object_id)) + N'.' + QUOTENAME(OBJECT_NAME(si.object_id))),
|
1200
|
+
COUNT(*)
|
1201
|
+
FROM [xmigra].[indexes] xi
|
1202
|
+
INNER JOIN sys.indexes si ON si.[name] = xi.[name]
|
1203
|
+
WHERE xi.[IndexID] NOT IN (
|
1204
|
+
SELECT [IndexID]
|
1205
|
+
FROM [xmigra].[updated_indexes]
|
1206
|
+
)
|
1207
|
+
GROUP BY xi.[name];
|
1208
|
+
|
1209
|
+
OPEN Index_cursor;
|
1210
|
+
|
1211
|
+
FETCH NEXT FROM Index_cursor INTO @index_name, @table_name, @match_count;
|
1212
|
+
|
1213
|
+
WHILE @@FETCH_STATUS = 0 BEGIN
|
1214
|
+
IF @match_count > 1
|
1215
|
+
BEGIN
|
1216
|
+
RAISERROR(N'Multiple indexes are named %s', 11, 1, @index_name);
|
1217
|
+
END;
|
1218
|
+
|
1219
|
+
SET @sqlcmd = N'DROP INDEX ' + @index_name + N' ON ' + @table_name + N';';
|
1220
|
+
EXEC sp_executesql @sqlcmd;
|
1221
|
+
PRINT N' Removed ' + @index_name + N'.';
|
1222
|
+
|
1223
|
+
FETCH NEXT FROM Index_cursor INTO @index_name, @table_name, @match_count;
|
1224
|
+
END;
|
1225
|
+
|
1226
|
+
CLOSE Index_cursor;
|
1227
|
+
DEALLOCATE Index_cursor;
|
1228
|
+
|
1229
|
+
DELETE FROM [xmigra].[indexes]
|
1230
|
+
WHERE [IndexID] NOT IN (
|
1231
|
+
SELECT ui.[IndexID]
|
1232
|
+
FROM [xmigra].[updated_indexes] ui
|
1233
|
+
);
|
1234
|
+
END_OF_SQL
|
1235
|
+
end
|
1236
|
+
|
1237
|
+
def remove_undesired_statistics_sql
|
1238
|
+
<<-"END_OF_SQL"
|
1239
|
+
PRINT N'Removing undesired statistics objects:';
|
1240
|
+
-- Iterate over statistics in [xmigra].[statistics] that don't have an entry in
|
1241
|
+
-- [xmigra].[updated_statistics].
|
1242
|
+
DECLARE @sqlcmd NVARCHAR(1000); -- Built SQL command
|
1243
|
+
DECLARE @statsobj_name NVARCHAR(256); -- Name of statistics object to drop
|
1244
|
+
DECLARE @table_name SYSNAME; -- Name of table owning the statistics object
|
1245
|
+
DECLARE @match_count INT; -- Number of matching statistics object names
|
1246
|
+
|
1247
|
+
DECLARE Stats_cursor CURSOR LOCAL FOR
|
1248
|
+
SELECT
|
1249
|
+
QUOTENAME(xs.[Name]),
|
1250
|
+
MAX(QUOTENAME(OBJECT_SCHEMA_NAME(ss.object_id)) + N'.' + QUOTENAME(OBJECT_NAME(ss.object_id))),
|
1251
|
+
COUNT(ss.object_id)
|
1252
|
+
FROM [xmigra].[statistics] xs
|
1253
|
+
INNER JOIN sys.stats ss ON ss.[name] = xs.[Name]
|
1254
|
+
WHERE xs.[Columns] NOT IN (
|
1255
|
+
SELECT us.[Columns]
|
1256
|
+
FROM [xmigra].[updated_statistics] us
|
1257
|
+
WHERE us.[Name] = xs.[Name]
|
1258
|
+
)
|
1259
|
+
GROUP BY xs.[Name];
|
1260
|
+
|
1261
|
+
OPEN Stats_cursor;
|
1262
|
+
|
1263
|
+
FETCH NEXT FROM Stats_cursor INTO @statsobj_name, @table_name, @match_count;
|
1264
|
+
|
1265
|
+
WHILE @@FETCH_STATUS = 0 BEGIN
|
1266
|
+
IF @match_count > 1
|
1267
|
+
BEGIN
|
1268
|
+
RAISERROR(N'Multiple indexes are named %s', 11, 1, @statsobj_name);
|
1269
|
+
END;
|
1270
|
+
|
1271
|
+
SET @sqlcmd = N'DROP STATISTICS ' + @table_name + N'.' + @statsobj_name + N';';
|
1272
|
+
EXEC sp_executesql @sqlcmd;
|
1273
|
+
PRINT N' Removed statistics object ' + @statsobj_name + N'.'
|
1274
|
+
|
1275
|
+
FETCH NEXT FROM Stats_cursor INTO @statsobj_name, @table_name, @match_count;
|
1276
|
+
END;
|
1277
|
+
|
1278
|
+
CLOSE Stats_cursor;
|
1279
|
+
DEALLOCATE Stats_cursor;
|
1280
|
+
|
1281
|
+
DELETE FROM [xmigra].[statistics]
|
1282
|
+
WHERE [Columns] NOT IN (
|
1283
|
+
SELECT us.[Columns]
|
1284
|
+
FROM [xmigra].[updated_statistics] us
|
1285
|
+
WHERE us.[Name] = [Name]
|
1286
|
+
);
|
1287
|
+
END_OF_SQL
|
1288
|
+
end
|
1289
|
+
|
1290
|
+
def create_new_indexes_sql
|
1291
|
+
indexes.collect do |index|
|
1292
|
+
index_id_literal = MSSQLSpecifics.string_literal(index.id)
|
1293
|
+
index_name_literal = MSSQLSpecifics.string_literal(index.name)
|
1294
|
+
<<-"END_OF_SQL"
|
1295
|
+
PRINT N'Index ' + #{index_id_literal} + ':';
|
1296
|
+
IF EXISTS(
|
1297
|
+
SELECT * FROM [xmigra].[updated_indexes] ui
|
1298
|
+
WHERE ui.[IndexID] = #{index_id_literal}
|
1299
|
+
AND ui.[IndexID] NOT IN (
|
1300
|
+
SELECT i.[IndexID] FROM [xmigra].[indexes] i
|
1301
|
+
)
|
1302
|
+
)
|
1303
|
+
BEGIN
|
1304
|
+
IF EXISTS (
|
1305
|
+
SELECT * FROM sys.indexes
|
1306
|
+
WHERE [name] = #{index_name_literal}
|
1307
|
+
)
|
1308
|
+
BEGIN
|
1309
|
+
RAISERROR(N'An index already exists named %s', 11, 1, #{index_name_literal});
|
1310
|
+
END;
|
1311
|
+
|
1312
|
+
PRINT N' Creating...';
|
1313
|
+
#{index.definition_sql};
|
1314
|
+
|
1315
|
+
IF (SELECT COUNT(*) FROM sys.indexes WHERE [name] = #{index_name_literal}) <> 1
|
1316
|
+
BEGIN
|
1317
|
+
RAISERROR(N'Index %s was not created by its definition.', 11, 1,
|
1318
|
+
#{index_name_literal});
|
1319
|
+
END;
|
1320
|
+
|
1321
|
+
INSERT INTO [xmigra].[indexes] ([IndexID], [name])
|
1322
|
+
VALUES (#{index_id_literal}, #{index_name_literal});
|
1323
|
+
END
|
1324
|
+
ELSE
|
1325
|
+
BEGIN
|
1326
|
+
PRINT N' Already exists.';
|
1327
|
+
END;
|
1328
|
+
END_OF_SQL
|
1329
|
+
end.join(ddl_block_separator)
|
1330
|
+
end
|
1331
|
+
|
1332
|
+
def create_new_statistics_sql
|
1333
|
+
stats_objs.collect do |stats_obj|
|
1334
|
+
stats_name = MSSQLSpecifics.string_literal(stats_obj.name)
|
1335
|
+
strlit = lambda {|s| MSSQLSpecifics.string_literal(s)}
|
1336
|
+
|
1337
|
+
stats_obj.creation_sql
|
1338
|
+
<<-"END_OF_SQL"
|
1339
|
+
PRINT N'Statistics object #{stats_obj.name}:';
|
1340
|
+
IF EXISTS (
|
1341
|
+
SELECT * FROM [xmigra].[updated_statistics] us
|
1342
|
+
WHERE us.[Name] = #{stats_name}
|
1343
|
+
AND us.[Columns] NOT IN (
|
1344
|
+
SELECT s.[Columns]
|
1345
|
+
FROM [xmigra].[statistics] s
|
1346
|
+
WHERE s.[Name] = us.[Name]
|
1347
|
+
)
|
1348
|
+
)
|
1349
|
+
BEGIN
|
1350
|
+
IF EXISTS (
|
1351
|
+
SELECT * FROM sys.stats
|
1352
|
+
WHERE [name] = #{stats_name}
|
1353
|
+
)
|
1354
|
+
BEGIN
|
1355
|
+
RAISERROR(N'A statistics object named %s already exists.', 11, 1, #{stats_name})
|
1356
|
+
END;
|
1357
|
+
|
1358
|
+
PRINT N' Creating...';
|
1359
|
+
#{stats_obj.creation_sql}
|
1360
|
+
|
1361
|
+
INSERT INTO [xmigra].[statistics] ([Name], [Columns])
|
1362
|
+
VALUES (#{stats_name}, #{strlit[stats_obj.columns]})
|
1363
|
+
END
|
1364
|
+
ELSE
|
1365
|
+
BEGIN
|
1366
|
+
PRINT N' Already exists.';
|
1367
|
+
END;
|
1368
|
+
END_OF_SQL
|
1369
|
+
end.join(ddl_block_separator)
|
1370
|
+
end
|
1371
|
+
|
1372
|
+
def upgrade_cleanup_sql
|
1373
|
+
<<-"END_OF_SQL"
|
1374
|
+
PRINT N'Cleaning up from the upgrade:';
|
1375
|
+
DROP TABLE [xmigra].[migrations];
|
1376
|
+
DROP TABLE [xmigra].[updated_indexes];
|
1377
|
+
DROP TABLE [xmigra].[updated_statistics];
|
1378
|
+
END_OF_SQL
|
1379
|
+
end
|
1380
|
+
|
1381
|
+
def ensure_permissions_table_sql
|
1382
|
+
strlit = MSSQLSpecifics.method(:string_literal)
|
1383
|
+
<<-"END_OF_SQL"
|
1384
|
+
-- ------------ SET UP XMIGRA PERMISSION TRACKING OBJECTS ------------ --
|
1385
|
+
|
1386
|
+
PRINT N'Setting up XMigra permission tracking:';
|
1387
|
+
IF NOT EXISTS (
|
1388
|
+
SELECT * FROM sys.schemas
|
1389
|
+
WHERE name = N'xmigra'
|
1390
|
+
)
|
1391
|
+
BEGIN
|
1392
|
+
EXEC sp_executesql N'
|
1393
|
+
CREATE SCHEMA [xmigra] AUTHORIZATION [dbo];
|
1394
|
+
';
|
1395
|
+
END;
|
1396
|
+
GO
|
1397
|
+
|
1398
|
+
IF NOT EXISTS(
|
1399
|
+
SELECT * FROM sys.objects
|
1400
|
+
WHERE object_id = OBJECT_ID(N'[xmigra].[revokable_permissions]')
|
1401
|
+
AND type IN (N'U')
|
1402
|
+
)
|
1403
|
+
BEGIN
|
1404
|
+
CREATE TABLE [xmigra].[revokable_permissions] (
|
1405
|
+
[permissions] nvarchar(200) NOT NULL,
|
1406
|
+
[object] nvarchar(260) NOT NULL,
|
1407
|
+
[principal_id] int NOT NULL
|
1408
|
+
) ON [PRIMARY];
|
1409
|
+
END;
|
1410
|
+
GO
|
1411
|
+
|
1412
|
+
IF EXISTS(
|
1413
|
+
SELECT * FROM sys.objects
|
1414
|
+
WHERE object_id = OBJECT_ID(N'[xmigra].[ip_prepare_revoke]')
|
1415
|
+
AND type IN (N'P', N'PC')
|
1416
|
+
)
|
1417
|
+
BEGIN
|
1418
|
+
DROP PROCEDURE [xmigra].[ip_prepare_revoke];
|
1419
|
+
END;
|
1420
|
+
GO
|
1421
|
+
|
1422
|
+
CREATE PROCEDURE [xmigra].[ip_prepare_revoke]
|
1423
|
+
(
|
1424
|
+
@permissions nvarchar(200),
|
1425
|
+
@object nvarchar(260),
|
1426
|
+
@principal sysname
|
1427
|
+
)
|
1428
|
+
AS
|
1429
|
+
BEGIN
|
1430
|
+
INSERT INTO [xmigra].[revokable_permissions] ([permissions], [object], [principal_id])
|
1431
|
+
VALUES (@permissions, @object, DATABASE_PRINCIPAL_ID(@principal));
|
1432
|
+
END;
|
1433
|
+
END_OF_SQL
|
1434
|
+
end
|
1435
|
+
|
1436
|
+
def revoke_previous_permissions_sql
|
1437
|
+
<<-"END_OF_SQL"
|
1438
|
+
|
1439
|
+
-- ------------- REVOKING PREVIOUSLY GRANTED PERMISSIONS ------------- --
|
1440
|
+
|
1441
|
+
PRINT N'Revoking previously granted permissions:';
|
1442
|
+
-- Iterate over permissions listed in [xmigra].[revokable_permissions]
|
1443
|
+
DECLARE @sqlcmd NVARCHAR(1000); -- Built SQL command
|
1444
|
+
DECLARE @permissions NVARCHAR(200);
|
1445
|
+
DECLARE @object NVARCHAR(260);
|
1446
|
+
DECLARE @principal NVARCHAR(150);
|
1447
|
+
|
1448
|
+
DECLARE Permission_cursor CURSOR LOCAL FOR
|
1449
|
+
SELECT
|
1450
|
+
xp.[permissions],
|
1451
|
+
xp.[object],
|
1452
|
+
QUOTENAME(sdp.name)
|
1453
|
+
FROM [xmigra].[revokable_permissions] xp
|
1454
|
+
INNER JOIN sys.database_principals sdp ON xp.principal_id = sdp.principal_id;
|
1455
|
+
|
1456
|
+
OPEN Permission_cursor;
|
1457
|
+
|
1458
|
+
FETCH NEXT FROM Permission_cursor INTO @permissions, @object, @principal;
|
1459
|
+
|
1460
|
+
WHILE @@FETCH_STATUS = 0 BEGIN
|
1461
|
+
SET @sqlcmd = N'REVOKE ' + @permissions + N' ON ' + @object + ' FROM ' + @principal + N';';
|
1462
|
+
BEGIN TRY
|
1463
|
+
EXEC sp_executesql @sqlcmd;
|
1464
|
+
END TRY
|
1465
|
+
BEGIN CATCH
|
1466
|
+
END CATCH
|
1467
|
+
|
1468
|
+
FETCH NEXT FROM Permission_cursor INTO @permissions, @object, @principal;
|
1469
|
+
END;
|
1470
|
+
|
1471
|
+
CLOSE Permission_cursor;
|
1472
|
+
DEALLOCATE Permission_cursor;
|
1473
|
+
|
1474
|
+
DELETE FROM [xmigra].[revokable_permissions];
|
1475
|
+
END_OF_SQL
|
1476
|
+
end
|
1477
|
+
|
1478
|
+
def granting_permissions_comment_sql
|
1479
|
+
<<-"END_OF_SQL"
|
1480
|
+
|
1481
|
+
-- ---------------------- GRANTING PERMISSIONS ----------------------- --
|
1482
|
+
|
1483
|
+
END_OF_SQL
|
1484
|
+
end
|
1485
|
+
|
1486
|
+
def grant_permissions_sql(permissions, object, principal)
|
1487
|
+
strlit = MSSQLSpecifics.method(:string_literal)
|
1488
|
+
permissions_string = permissions.to_a.join(', ')
|
1489
|
+
|
1490
|
+
<<-"END_OF_SQL"
|
1491
|
+
PRINT N'Granting #{permissions_string} on #{object} to #{principal}:';
|
1492
|
+
GRANT #{permissions_string} ON #{object} TO #{principal};
|
1493
|
+
EXEC [xmigra].[ip_prepare_revoke] #{strlit[permissions_string]}, #{strlit[object]}, #{strlit[principal]};
|
1494
|
+
END_OF_SQL
|
1495
|
+
end
|
1496
|
+
|
1497
|
+
def insert_access_creation_record_sql
|
1498
|
+
name_literal = MSSQLSpecifics.string_literal(quoted_name)
|
1499
|
+
|
1500
|
+
<<-"END_OF_SQL"
|
1501
|
+
INSERT INTO [xmigra].[access_objects] ([type], [name])
|
1502
|
+
VALUES (N'#{self.class::OBJECT_TYPE}', #{name_literal});
|
1503
|
+
END_OF_SQL
|
1504
|
+
end
|
1505
|
+
|
1506
|
+
# Call on an extended Migration object to get the SQL to execute.
|
1507
|
+
def migration_application_sql
|
1508
|
+
id_literal = MSSQLSpecifics.string_literal(id)
|
1509
|
+
template = <<-"END_OF_SQL"
|
1510
|
+
IF EXISTS (
|
1511
|
+
SELECT * FROM [xmigra].[migrations]
|
1512
|
+
WHERE [MigrationID] = #{id_literal}
|
1513
|
+
AND [Install] <> 0
|
1514
|
+
)
|
1515
|
+
BEGIN
|
1516
|
+
PRINT #{MSSQLSpecifics.string_literal('Applying "' + File.basename(file_path) + '":')};
|
1517
|
+
|
1518
|
+
%s
|
1519
|
+
|
1520
|
+
INSERT INTO [xmigra].[applied] ([MigrationID], [Description])
|
1521
|
+
VALUES (#{id_literal}, #{MSSQLSpecifics.string_literal(description)});
|
1522
|
+
END;
|
1523
|
+
END_OF_SQL
|
1524
|
+
|
1525
|
+
parts = []
|
1526
|
+
|
1527
|
+
each_batch(sql) do |batch|
|
1528
|
+
parts << batch
|
1529
|
+
end
|
1530
|
+
|
1531
|
+
return (template % parts.collect do |batch|
|
1532
|
+
"EXEC sp_executesql @statement = " + MSSQLSpecifics.string_literal(batch) + ";"
|
1533
|
+
end.join("\n"))
|
1534
|
+
end
|
1535
|
+
|
1536
|
+
def each_batch(sql)
|
1537
|
+
current_batch_lines = []
|
1538
|
+
sql.each_line do |line|
|
1539
|
+
if line.strip.upcase == 'GO'
|
1540
|
+
batch = current_batch_lines.join('')
|
1541
|
+
yield batch unless batch.strip.empty?
|
1542
|
+
current_batch_lines.clear
|
1543
|
+
else
|
1544
|
+
current_batch_lines << line
|
1545
|
+
end
|
1546
|
+
end
|
1547
|
+
unless current_batch_lines.empty?
|
1548
|
+
batch = current_batch_lines.join('')
|
1549
|
+
yield batch unless batch.strip.empty?
|
1550
|
+
end
|
1551
|
+
end
|
1552
|
+
|
1553
|
+
def batch_separator
|
1554
|
+
"GO\n"
|
1555
|
+
end
|
1556
|
+
|
1557
|
+
def check_existence_sql(for_existence, error_message)
|
1558
|
+
error_message = sprintf(error_message, quoted_name)
|
1559
|
+
|
1560
|
+
return <<-"END_OF_SQL"
|
1561
|
+
|
1562
|
+
IF #{"NOT" if for_existence} #{existence_test_sql}
|
1563
|
+
RAISERROR(N'#{error_message}', 11, 1);
|
1564
|
+
END_OF_SQL
|
1565
|
+
end
|
1566
|
+
|
1567
|
+
def creation_notice
|
1568
|
+
return "PRINT " + MSSQLSpecifics.string_literal("Creating #{printable_type} #{quoted_name}:") + ";"
|
1569
|
+
end
|
1570
|
+
|
1571
|
+
def name_parts
|
1572
|
+
if m = DBNAME_PATTERN.match(name)
|
1573
|
+
[m[1], m[2]].compact.collect do |p|
|
1574
|
+
MSSQLSpecifics.strip_identifier_quoting(p)
|
1575
|
+
end
|
1576
|
+
else
|
1577
|
+
raise XMigra::Error, "Invalid database object name"
|
1578
|
+
end
|
1579
|
+
end
|
1580
|
+
|
1581
|
+
def quoted_name
|
1582
|
+
name_parts.collect do |p|
|
1583
|
+
"[]".insert(1, p)
|
1584
|
+
end.join('.')
|
1585
|
+
end
|
1586
|
+
|
1587
|
+
def object_type_codes
|
1588
|
+
MSSQLSpecifics.object_type_codes(self)
|
1589
|
+
end
|
1590
|
+
|
1591
|
+
def existence_test_sql
|
1592
|
+
object_type_list = object_type_codes.collect {|t| "N'#{t}'"}.join(', ')
|
1593
|
+
|
1594
|
+
return <<-"END_OF_SQL"
|
1595
|
+
EXISTS (
|
1596
|
+
SELECT * FROM sys.objects
|
1597
|
+
WHERE object_id = OBJECT_ID(N'#{quoted_name}')
|
1598
|
+
AND type IN (#{object_type_list})
|
1599
|
+
)
|
1600
|
+
END_OF_SQL
|
1601
|
+
end
|
1602
|
+
|
1603
|
+
def branch_id_literal
|
1604
|
+
@mssql_branch_id_literal ||= MSSQLSpecifics.string_literal(XMigra.secure_digest(branch_identifier))
|
1605
|
+
end
|
1606
|
+
|
1607
|
+
def upgrading_to_new_branch_test_sql
|
1608
|
+
(<<-"END_OF_SQL").chomp
|
1609
|
+
(EXISTS (
|
1610
|
+
SELECT TOP(1) * FROM [xmigra].[branch_upgrade]
|
1611
|
+
WHERE [Next] = #{branch_id_literal}
|
1612
|
+
))
|
1613
|
+
END_OF_SQL
|
1614
|
+
end
|
1615
|
+
|
1616
|
+
def branch_upgrade_sql
|
1617
|
+
parts = [<<-"END_OF_SQL"]
|
1618
|
+
IF #{upgrading_to_new_branch_test_sql}
|
1619
|
+
BEGIN
|
1620
|
+
PRINT N'Migrating from previous schema branch:';
|
1621
|
+
|
1622
|
+
DECLARE @sqlcmd NVARCHAR(MAX);
|
1623
|
+
|
1624
|
+
DECLARE CmdCursor CURSOR LOCAL FOR
|
1625
|
+
SELECT bu.[UpgradeSql]
|
1626
|
+
FROM [xmigra].[branch_upgrade] bu
|
1627
|
+
WHERE bu.[Next] = #{branch_id_literal}
|
1628
|
+
ORDER BY bu.[ApplicationOrder] ASC;
|
1629
|
+
|
1630
|
+
OPEN CmdCursor;
|
1631
|
+
|
1632
|
+
FETCH NEXT FROM CmdCursor INTO @sqlcmd;
|
1633
|
+
|
1634
|
+
WHILE @@FETCH_STATUS = 0 BEGIN
|
1635
|
+
EXECUTE sp_executesql @sqlcmd;
|
1636
|
+
|
1637
|
+
FETCH NEXT FROM CmdCursor INTO @sqlcmd;
|
1638
|
+
END;
|
1639
|
+
|
1640
|
+
CLOSE CmdCursor;
|
1641
|
+
DEALLOCATE CmdCursor;
|
1642
|
+
|
1643
|
+
DECLARE @applied NVARCHAR(80);
|
1644
|
+
DECLARE @old_branch NVARCHAR(80);
|
1645
|
+
|
1646
|
+
SELECT TOP(1) @applied = [CompletesMigration], @old_branch = [Current]
|
1647
|
+
FROM [xmigra].[branch_upgrade]
|
1648
|
+
WHERE [Next] = #{branch_id_literal};
|
1649
|
+
|
1650
|
+
-- Delete the "applied" record for the migration if there was one, so that
|
1651
|
+
-- a new record with this ID can be inserted.
|
1652
|
+
DELETE FROM [xmigra].[applied] WHERE [MigrationID] = @applied;
|
1653
|
+
|
1654
|
+
-- Create a "version bridge" record in the "applied" table for the branch upgrade
|
1655
|
+
INSERT INTO [xmigra].[applied] ([MigrationID], [VersionBridgeMark], [Description])
|
1656
|
+
VALUES (@applied, 1, N'Branch upgrade from branch ' + @old_branch);
|
1657
|
+
END;
|
1658
|
+
|
1659
|
+
DELETE FROM [xmigra].[branch_upgrade];
|
1660
|
+
|
1661
|
+
END_OF_SQL
|
1662
|
+
|
1663
|
+
if branch_upgrade.applicable? migrations
|
1664
|
+
batch_template = <<-"END_OF_SQL"
|
1665
|
+
INSERT INTO [xmigra].[branch_upgrade]
|
1666
|
+
([Current], [Next], [CompletesMigration], [UpgradeSql])
|
1667
|
+
VALUES (
|
1668
|
+
#{branch_id_literal},
|
1669
|
+
#{MSSQLSpecifics.string_literal(branch_upgrade.target_branch)},
|
1670
|
+
#{MSSQLSpecifics.string_literal(branch_upgrade.migration_completed_id)},
|
1671
|
+
%s
|
1672
|
+
);
|
1673
|
+
END_OF_SQL
|
1674
|
+
|
1675
|
+
each_batch(branch_upgrade.sql) do |batch|
|
1676
|
+
# Insert the batch into the [xmigra].[branch_upgrade] table
|
1677
|
+
parts << (batch_template % MSSQLSpecifics.string_literal(batch))
|
1678
|
+
end
|
1679
|
+
else
|
1680
|
+
# Insert a placeholder that only declares the current branch of the schema
|
1681
|
+
parts << <<-"END_OF_SQL"
|
1682
|
+
INSERT INTO [xmigra].[branch_upgrade] ([Current]) VALUES (#{branch_id_literal});
|
1683
|
+
END_OF_SQL
|
1684
|
+
end
|
1685
|
+
|
1686
|
+
return parts.join("\n")
|
1687
|
+
end
|
1688
|
+
|
1689
|
+
class << self
|
1690
|
+
def strip_identifier_quoting(s)
|
1691
|
+
case
|
1692
|
+
when s.empty? then return s
|
1693
|
+
when s[0,1] == "[" && s[-1,1] == "]" then return s[1..-2]
|
1694
|
+
when s[0,1] == '"' && s[-1,1] == '"' then return s[1..-2]
|
1695
|
+
else return s
|
1696
|
+
end
|
1697
|
+
end
|
1698
|
+
|
1699
|
+
def object_type_codes(type)
|
1700
|
+
case type
|
1701
|
+
when StoredProcedure then %w{P PC}
|
1702
|
+
when View then ['V']
|
1703
|
+
when Function then %w{AF FN FS FT IF TF}
|
1704
|
+
end
|
1705
|
+
end
|
1706
|
+
|
1707
|
+
def string_literal(s)
|
1708
|
+
"N'#{s.gsub("'","''")}'"
|
1709
|
+
end
|
1710
|
+
end
|
1711
|
+
end
|
1712
|
+
|
1713
|
+
class VersionControlError < XMigra::Error; end
|
1714
|
+
|
1715
|
+
module SubversionSpecifics
|
1716
|
+
PRODUCTION_PATH_PROPERTY = 'xmigra:production-path'
|
1717
|
+
|
1718
|
+
class << self
|
1719
|
+
def manages(path)
|
1720
|
+
begin
|
1721
|
+
return true if File.directory?(File.join(path, '.svn'))
|
1722
|
+
rescue TypeError
|
1723
|
+
return false
|
1724
|
+
end
|
1725
|
+
|
1726
|
+
`svn info "#{path}" 2>&1`
|
1727
|
+
return $?.success?
|
1728
|
+
end
|
1729
|
+
|
1730
|
+
# Run the svn command line client in XML mode and return a REXML::Document
|
1731
|
+
def run_svn(subcmd, *args)
|
1732
|
+
options = (Hash === args[-1]) ? args.pop : {}
|
1733
|
+
no_result = !options.fetch(:get_result, true)
|
1734
|
+
raw_result = options.fetch(:raw, false)
|
1735
|
+
|
1736
|
+
cmd_parts = ["svn", subcmd.to_s]
|
1737
|
+
cmd_parts << "--xml" unless no_result || raw_result
|
1738
|
+
cmd_parts.concat(
|
1739
|
+
args.collect {|a| '""'.insert(1, a)}
|
1740
|
+
)
|
1741
|
+
cmd_str = cmd_parts.join(' ')
|
1742
|
+
|
1743
|
+
output = `#{cmd_str}`
|
1744
|
+
raise(VersionControlError, "Subversion command failed with exit code #{$?.exitstatus}") unless $?.success?
|
1745
|
+
return output if raw_result && !no_result
|
1746
|
+
return REXML::Document.new(output) unless no_result
|
1747
|
+
end
|
1748
|
+
end
|
1749
|
+
|
1750
|
+
def subversion(*args)
|
1751
|
+
SubversionSpecifics.run_svn(*args)
|
1752
|
+
end
|
1753
|
+
|
1754
|
+
def check_working_copy!
|
1755
|
+
return unless production
|
1756
|
+
|
1757
|
+
schema_info = subversion_info
|
1758
|
+
file_paths = Array.from_generator(method(:each_file_path))
|
1759
|
+
status = subversion(:status, '--no-ignore', path)
|
1760
|
+
unversioned_files = status.elements.each("status/target/entry/@path")
|
1761
|
+
unversioned_files = unversioned_files.collect {|a| File.expand_path(a.to_s)}
|
1762
|
+
|
1763
|
+
unless (file_paths & unversioned_files).empty?
|
1764
|
+
raise VersionControlError, "Some source files are not versions found in the repository"
|
1765
|
+
end
|
1766
|
+
status = nil
|
1767
|
+
|
1768
|
+
wc_rev = {}
|
1769
|
+
working_rev = schema_info.elements["info/entry/@revision"].value.to_i
|
1770
|
+
file_paths.each do |fp|
|
1771
|
+
fp_info = subversion(:info, fp)
|
1772
|
+
wc_rev[fp] = fp_wc_rev = fp_info.elements["info/entry/@revision"].value.to_i
|
1773
|
+
if working_rev != fp_wc_rev
|
1774
|
+
raise VersionControlError, "The working copy contains objects at multiple revisions"
|
1775
|
+
end
|
1776
|
+
end
|
1777
|
+
|
1778
|
+
migrations.each do |m|
|
1779
|
+
fpath = m.file_path
|
1780
|
+
|
1781
|
+
log = subversion(:log, "-r#{wc_rev[fpath]}:1", "--stop-on-copy", fpath)
|
1782
|
+
if log.elements["log/logentry[2]"]
|
1783
|
+
raise VersionControlError, "'#{fpath}' has been modified in the repository since it was created or copied"
|
1784
|
+
end
|
1785
|
+
end
|
1786
|
+
|
1787
|
+
# Since a production script was requested, warn if we are not generating
|
1788
|
+
# from a production branch
|
1789
|
+
if branch_use != :production and self.respond_to? :warning
|
1790
|
+
self.warning(<<END_OF_MESSAGE)
|
1791
|
+
The branch backing the target working copy is not marked as a production branch.
|
1792
|
+
END_OF_MESSAGE
|
1793
|
+
end
|
1794
|
+
end
|
1795
|
+
|
1796
|
+
def vcs_information
|
1797
|
+
info = subversion_info
|
1798
|
+
return [
|
1799
|
+
"Repository URL: #{info.elements["info/entry/url"].text}",
|
1800
|
+
"Revision: #{info.elements["info/entry/@revision"].value}"
|
1801
|
+
].join("\n")
|
1802
|
+
end
|
1803
|
+
|
1804
|
+
def get_conflict_info
|
1805
|
+
# Check if the structure head is conflicted
|
1806
|
+
structure_dir = Pathname.new(self.path) + SchemaManipulator::STRUCTURE_SUBDIR
|
1807
|
+
status = subversion(:status, structure_dir + MigrationChain::HEAD_FILE)
|
1808
|
+
return nil if status.elements["status/target/entry/wc-status/@item"].value != "conflicted"
|
1809
|
+
|
1810
|
+
chain_head = lambda do |extension|
|
1811
|
+
pattern = MigrationChain::HEAD_FILE + extension
|
1812
|
+
if extension.include? '*'
|
1813
|
+
files = structure_dir.glob(MigrationChain::HEAD_FILE + extension)
|
1814
|
+
raise VersionControlError, "Multiple #{pattern} files in structure directory" if files.length > 1
|
1815
|
+
raise VersionControlError, "#{pattern} file missing from structure directory" if files.length < 1
|
1816
|
+
else
|
1817
|
+
files = [structure_dir.join(pattern)]
|
1818
|
+
end
|
1819
|
+
|
1820
|
+
# Using YAML.parse_file and YAML::Syck::Node#transform rerenders
|
1821
|
+
# scalars in the same style they were read from the source file:
|
1822
|
+
return YAML.parse_file(files[0]).transform
|
1823
|
+
end
|
1824
|
+
|
1825
|
+
if (structure_dir + (MigrationChain::HEAD_FILE + ".working")).exist?
|
1826
|
+
# This is a merge conflict
|
1827
|
+
|
1828
|
+
# structure/head.yaml.working is from the current branch
|
1829
|
+
# structure/head.yaml.merge-left.r* is the branch point
|
1830
|
+
# structure/head.yaml.merge-right.r* is from the merged-in branch
|
1831
|
+
this_head = chain_head.call(".working")
|
1832
|
+
other_head = chain_head.call(".merge-right.r*")
|
1833
|
+
branch_point = chain_head.call(".merge-left.r*")[MigrationChain::LATEST_CHANGE]
|
1834
|
+
|
1835
|
+
conflict = MigrationConflict.new(structure_dir, branch_point, [other_head, this_head])
|
1836
|
+
|
1837
|
+
branch_use {|u| conflict.branch_use = u}
|
1838
|
+
else
|
1839
|
+
# This is an update conflict
|
1840
|
+
|
1841
|
+
# structure/head.yaml.mine is from the working copy
|
1842
|
+
# structure/head.yaml.r<lower> is the common ancestor
|
1843
|
+
# structure/head.yaml.r<higher> is the newer revision
|
1844
|
+
working_head = chain_head.call('.mine')
|
1845
|
+
oldrev, newrev = nil, 0
|
1846
|
+
structure_dir.glob(MigrationChain::HEAD_FILE + '.r*') do |fn|
|
1847
|
+
if fn.to_s =~ /.r(\d+)$/
|
1848
|
+
rev = $1.to_i
|
1849
|
+
if oldrev.nil? or rev < oldrev
|
1850
|
+
oldrev = rev
|
1851
|
+
end
|
1852
|
+
if newrev < rev
|
1853
|
+
newrev = rev
|
1854
|
+
end
|
1855
|
+
end
|
1856
|
+
end
|
1857
|
+
repo_head = chain_head.call(".r#{newrev}")
|
1858
|
+
branch_point = chain_head.call(".r#{oldrev}")[MigrationChain::LATEST_CHANGE]
|
1859
|
+
|
1860
|
+
conflict = MigrationConflict.new(structure_dir, branch_point, [repo_head, working_head])
|
1861
|
+
branch_use {|u| conflict.branch_use = u}
|
1862
|
+
|
1863
|
+
fix_target, = conflict.migration_tweak
|
1864
|
+
fix_target_st = subversion(:status, fix_target)
|
1865
|
+
if fix_target_st.elements['status/target/entry/wc-status/@item'].value == 'modified'
|
1866
|
+
conflict.scope = :working_copy
|
1867
|
+
end
|
1868
|
+
end
|
1869
|
+
|
1870
|
+
tool = self
|
1871
|
+
conflict.after_fix = proc {tool.resolve_conflict!(structure_dir + MigrationChain::HEAD_FILE)}
|
1872
|
+
|
1873
|
+
return conflict
|
1874
|
+
end
|
1875
|
+
|
1876
|
+
def branch_use
|
1877
|
+
# Look for xmigra:production-path on the database directory (self.path)
|
1878
|
+
return nil unless prod_path_element = subversion(:propget, PRODUCTION_PATH_PROPERTY, self.path).elements['properties/target/property']
|
1879
|
+
|
1880
|
+
prod_path_pattern = Regexp.new(prod_path_element.text)
|
1881
|
+
|
1882
|
+
use = prod_path_pattern.match(branch_identifier) ? :production : :development
|
1883
|
+
if block_given?
|
1884
|
+
yield use
|
1885
|
+
else
|
1886
|
+
return use
|
1887
|
+
end
|
1888
|
+
end
|
1889
|
+
|
1890
|
+
def branch_identifier
|
1891
|
+
return @subversion_branch_id if defined? @subversion_branch_id
|
1892
|
+
dir_info = subversion_info
|
1893
|
+
return @subversion_branch_id = dir_info.elements['info/entry/url'].text[
|
1894
|
+
dir_info.elements['info/entry/repository/root'].text.length..-1
|
1895
|
+
]
|
1896
|
+
end
|
1897
|
+
|
1898
|
+
def production_pattern
|
1899
|
+
subversion(:propget, PRODUCTION_PATH_PROPERTY, self.path, :raw=>true)
|
1900
|
+
end
|
1901
|
+
def production_pattern=(pattern)
|
1902
|
+
subversion(:propset, PRODUCTION_PATH_PROPERTY, pattern, self.path, :get_result=>false)
|
1903
|
+
end
|
1904
|
+
|
1905
|
+
def resolve_conflict!(path)
|
1906
|
+
subversion(:resolve, '--accept=working', path, :get_result=>false)
|
1907
|
+
end
|
1908
|
+
|
1909
|
+
|
1910
|
+
def vcs_move(old_path, new_path)
|
1911
|
+
subversion(:move, old_path, new_path, :get_result=>false)
|
1912
|
+
end
|
1913
|
+
|
1914
|
+
def vcs_remove(path)
|
1915
|
+
subversion(:remove, path, :get_result=>false)
|
1916
|
+
end
|
1917
|
+
|
1918
|
+
def subversion_info
|
1919
|
+
return @subversion_info if defined? @subversion_info
|
1920
|
+
return @subversion_info = subversion(:info, self.path)
|
1921
|
+
end
|
1922
|
+
end
|
1923
|
+
|
1924
|
+
module GitSpecifics
|
1925
|
+
MASTER_HEAD_ATTRIBUTE = 'xmigra-master'
|
1926
|
+
MASTER_BRANCH_SUBDIR = 'xmigra-master'
|
1927
|
+
|
1928
|
+
class << self
|
1929
|
+
def manages(path)
|
1930
|
+
run_git(:status, :check_exit=>true)
|
1931
|
+
end
|
1932
|
+
|
1933
|
+
def run_git(subcmd, *args)
|
1934
|
+
options = (Hash === args[-1]) ? args.pop : {}
|
1935
|
+
check_exit = options.fetch(:check_exit, false)
|
1936
|
+
no_result = !options.fetch(:get_result, true)
|
1937
|
+
|
1938
|
+
cmd_parts = ["git", subcmd.to_s]
|
1939
|
+
cmd_parts.concat(
|
1940
|
+
args.flatten.collect {|a| '""'.insert(1, a.to_s)}
|
1941
|
+
)
|
1942
|
+
case PLATFORM
|
1943
|
+
when :unix
|
1944
|
+
cmd_parts << "2>/dev/null"
|
1945
|
+
end if options[:quiet]
|
1946
|
+
|
1947
|
+
cmd_str = cmd_parts.join(' ')
|
1948
|
+
|
1949
|
+
output = `#{cmd_str}`
|
1950
|
+
return ($?.success? ? output : nil) if options[:get_result] == :on_success
|
1951
|
+
return $?.success? if check_exit
|
1952
|
+
raise(VersionControlError, "Git command failed with exit code #{$?.exitstatus}") unless $?.success?
|
1953
|
+
return output unless no_result
|
1954
|
+
end
|
1955
|
+
|
1956
|
+
def attr_values(attr, path, options={})
|
1957
|
+
value_list = run_git('check-attr', attr, '--', path).each_line.map do |line|
|
1958
|
+
line.chomp.split(/: /, 3)[2]
|
1959
|
+
end
|
1960
|
+
return value_list unless options[:single]
|
1961
|
+
raise VersionControlError, options[:single] + ' ambiguous' if value_list.length > 1
|
1962
|
+
if (value_list.empty? || value_list == ['unspecified']) && options[:required]
|
1963
|
+
raise VersionControlError, options[:single] + ' undefined'
|
1964
|
+
end
|
1965
|
+
return value_list[0]
|
1966
|
+
end
|
1967
|
+
end
|
1968
|
+
|
1969
|
+
def git(*args)
|
1970
|
+
Dir.chdir(self.path) do |pwd|
|
1971
|
+
GitSpecifics.run_git(*args)
|
1972
|
+
end
|
1973
|
+
end
|
1974
|
+
|
1975
|
+
def check_working_copy!
|
1976
|
+
return unless production
|
1977
|
+
|
1978
|
+
file_paths = Array.from_generator(method(:each_file_path))
|
1979
|
+
unversioned_files = git(
|
1980
|
+
'diff-index',
|
1981
|
+
%w{-z --no-commit-id --name-only HEAD},
|
1982
|
+
'--',
|
1983
|
+
self.path
|
1984
|
+
).split("\000").collect do |path|
|
1985
|
+
File.expand_path(self.path + path)
|
1986
|
+
end
|
1987
|
+
|
1988
|
+
# Check that file_paths and unversioned_files are disjoint
|
1989
|
+
unless (file_paths & unversioned_files).empty?
|
1990
|
+
raise VersionControlError, "Some source files differ from their committed versions"
|
1991
|
+
end
|
1992
|
+
|
1993
|
+
git_fetch_master_branch
|
1994
|
+
migrations.each do |m|
|
1995
|
+
# Check that the migration has not changed in the currently checked-out branch
|
1996
|
+
fpath = m.file_path
|
1997
|
+
|
1998
|
+
history = git(:log, %w{--format=%H --}, fpath).split
|
1999
|
+
if history[1]
|
2000
|
+
raise VersionControlError, "'#{fpath}' has been modified in the current branch of the repository since its introduction"
|
2001
|
+
end
|
2002
|
+
end
|
2003
|
+
|
2004
|
+
# Since a production script was requested, warn if we are not generating
|
2005
|
+
# from a production branch
|
2006
|
+
if branch_use != :production
|
2007
|
+
raise VersionControlError, "The working tree is not a commit in the master history."
|
2008
|
+
end
|
2009
|
+
end
|
2010
|
+
|
2011
|
+
def vcs_information
|
2012
|
+
return [
|
2013
|
+
"Branch: #{branch_identifier}",
|
2014
|
+
"Path: #{git_internal_path}",
|
2015
|
+
"Commit: #{git_schema_commit}"
|
2016
|
+
].join("\n")
|
2017
|
+
end
|
2018
|
+
|
2019
|
+
def branch_identifier
|
2020
|
+
return (if self.production
|
2021
|
+
self.git_branch_info[0]
|
2022
|
+
else
|
2023
|
+
return @git_branch_identifier if defined? @git_branch_identifier
|
2024
|
+
|
2025
|
+
@git_branch_identifier = (
|
2026
|
+
self.git_master_head(:required=>false) ||
|
2027
|
+
self.git_local_branch_identifier(:note_modifications=>true)
|
2028
|
+
)
|
2029
|
+
end)
|
2030
|
+
end
|
2031
|
+
|
2032
|
+
def branch_use(commit=nil)
|
2033
|
+
if commit
|
2034
|
+
self.git_fetch_master_branch
|
2035
|
+
|
2036
|
+
# If there are no commits between the master head and *commit*, then
|
2037
|
+
# *commit* is production-ish
|
2038
|
+
return (self.git_commits_in? self.git_master_local_branch..commit) ? :development : :production
|
2039
|
+
end
|
2040
|
+
|
2041
|
+
return nil unless self.git_master_head(:required=>false)
|
2042
|
+
|
2043
|
+
return self.git_branch_info[1]
|
2044
|
+
end
|
2045
|
+
|
2046
|
+
def vcs_move(old_path, new_path)
|
2047
|
+
git(:mv, old_path, new_path, :get_result=>false)
|
2048
|
+
end
|
2049
|
+
|
2050
|
+
def vcs_remove(path)
|
2051
|
+
git(:rm, path, :get_result=>false)
|
2052
|
+
end
|
2053
|
+
|
2054
|
+
def production_pattern
|
2055
|
+
".+"
|
2056
|
+
end
|
2057
|
+
|
2058
|
+
def production_pattern=(pattern)
|
2059
|
+
raise VersionControlError, "Under version control by git, XMigra does not support production patterns."
|
2060
|
+
end
|
2061
|
+
|
2062
|
+
def get_conflict_info
|
2063
|
+
structure_dir = Pathname.new(self.path) + SchemaManipulator::STRUCTURE_SUBDIR
|
2064
|
+
head_file = structure_dir + MigrationChain::HEAD_FILE
|
2065
|
+
stage_numbers = []
|
2066
|
+
git('ls-files', '-uz', '--', head_file).split("\000").each {|ref|
|
2067
|
+
if m = /[0-7]{6} [0-9a-f]{40} (\d)\t\S*/.match(ref)
|
2068
|
+
stage_numbers |= [m[1].to_i]
|
2069
|
+
end
|
2070
|
+
}
|
2071
|
+
return nil unless stage_numbers.sort == [1, 2, 3]
|
2072
|
+
|
2073
|
+
chain_head = lambda do |stage_number|
|
2074
|
+
return YAML.parse(
|
2075
|
+
git(:show, ":#{stage_number}:#{head_file}")
|
2076
|
+
).transform
|
2077
|
+
end
|
2078
|
+
|
2079
|
+
# Ours (2) before theirs (3)...
|
2080
|
+
heads = [2, 3].collect(&chain_head)
|
2081
|
+
# ... unless merging from upstream
|
2082
|
+
if self.git_merging_from_upstream?
|
2083
|
+
heads.reverse!
|
2084
|
+
end
|
2085
|
+
|
2086
|
+
branch_point = chain_head.call(1)[MigrationChain::LATEST_CHANGE]
|
2087
|
+
|
2088
|
+
conflict = MigrationConflict.new(structure_dir, branch_point, heads)
|
2089
|
+
|
2090
|
+
# Standard git usage never commits directly to the master branch, and
|
2091
|
+
# there is no effective way to tell if this is happening.
|
2092
|
+
conflict.branch_use = :development
|
2093
|
+
|
2094
|
+
tool = self
|
2095
|
+
conflict.after_fix = proc {tool.resolve_conflict!(head_file)}
|
2096
|
+
|
2097
|
+
return conflict
|
2098
|
+
end
|
2099
|
+
|
2100
|
+
def resolve_conflict!(path)
|
2101
|
+
git(:add, '--', path, :get_result=>false)
|
2102
|
+
end
|
2103
|
+
|
2104
|
+
def git_master_head(options={})
|
2105
|
+
options = {:required=>true}.merge(options)
|
2106
|
+
return @git_master_head if defined? @git_master_head
|
2107
|
+
master_head = GitSpecifics.attr_values(
|
2108
|
+
MASTER_HEAD_ATTRIBUTE,
|
2109
|
+
self.path + SchemaManipulator::DBINFO_FILE,
|
2110
|
+
:single=>'Master branch',
|
2111
|
+
:required=>options[:required]
|
2112
|
+
)
|
2113
|
+
return nil if master_head.nil?
|
2114
|
+
return @git_master_head = master_head
|
2115
|
+
end
|
2116
|
+
|
2117
|
+
def git_branch
|
2118
|
+
return @git_branch if defined? @git_branch
|
2119
|
+
return @git_branch = git('rev-parse', %w{--abbrev-ref HEAD}).chomp
|
2120
|
+
end
|
2121
|
+
|
2122
|
+
def git_schema_commit
|
2123
|
+
return @git_commit if defined? @git_commit
|
2124
|
+
reported_commit = git(:log, %w{-n1 --format=%H --}, self.path).chomp
|
2125
|
+
raise VersionControlError, "Schema not committed" if reported_commit.empty?
|
2126
|
+
return @git_commit = reported_commit
|
2127
|
+
end
|
2128
|
+
|
2129
|
+
def git_branch_info
|
2130
|
+
return @git_branch_info if defined? @git_branch_info
|
2131
|
+
|
2132
|
+
self.git_fetch_master_branch
|
2133
|
+
|
2134
|
+
# If there are no commits between the master head and HEAD, this working
|
2135
|
+
# copy is production-ish
|
2136
|
+
return (@git_branch_info = if self.branch_use('HEAD') == :production
|
2137
|
+
[self.git_master_head, :production]
|
2138
|
+
else
|
2139
|
+
[self.git_local_branch_identifier, :development]
|
2140
|
+
end)
|
2141
|
+
end
|
2142
|
+
|
2143
|
+
def git_local_branch_identifier(options={})
|
2144
|
+
host = `hostname`
|
2145
|
+
path = git('rev-parse', '--show-toplevel')
|
2146
|
+
return "#{git_branch} of #{path} on #{host} (commit #{git_schema_commit})"
|
2147
|
+
end
|
2148
|
+
|
2149
|
+
def git_fetch_master_branch
|
2150
|
+
return if @git_master_branch_fetched
|
2151
|
+
master_url, remote_branch = self.git_master_head.split('#', 2)
|
2152
|
+
|
2153
|
+
git(:fetch, '-f', master_url, "#{remote_branch}:#{git_master_local_branch}", :get_result=>false, :quiet=>true)
|
2154
|
+
@git_master_branch_fetched = true
|
2155
|
+
end
|
2156
|
+
|
2157
|
+
def git_master_local_branch
|
2158
|
+
"#{MASTER_BRANCH_SUBDIR}/#{git_branch}"
|
2159
|
+
end
|
2160
|
+
|
2161
|
+
def git_internal_path
|
2162
|
+
return @git_internal_path if defined? @git_internal_path
|
2163
|
+
path_prefix = git('rev-parse', %w{--show-prefix}).chomp[0..-2]
|
2164
|
+
internal_path = '.'
|
2165
|
+
if path_prefix.length > 0
|
2166
|
+
internal_path += '/' + path_prefix
|
2167
|
+
end
|
2168
|
+
return @git_internal_path = internal_path
|
2169
|
+
end
|
2170
|
+
|
2171
|
+
def git_merging_from_upstream?
|
2172
|
+
upstream = git('rev-parse', '@{u}', :get_result=>:on_success, :quiet=>true)
|
2173
|
+
return false if upstream.nil?
|
2174
|
+
|
2175
|
+
# Check if there are any commits in #{upstream}..MERGE_HEAD
|
2176
|
+
begin
|
2177
|
+
return !(self.git_commits_in? upstream..'MERGE_HEAD')
|
2178
|
+
rescue VersionControlError
|
2179
|
+
return false
|
2180
|
+
end
|
2181
|
+
end
|
2182
|
+
|
2183
|
+
def git_commits_in?(range, path=nil)
|
2184
|
+
git(
|
2185
|
+
:log,
|
2186
|
+
'--pretty=format:%H',
|
2187
|
+
'-1',
|
2188
|
+
"#{range.begin.strip}..#{range.end.strip}",
|
2189
|
+
'--',
|
2190
|
+
path || self.path
|
2191
|
+
) != ''
|
2192
|
+
end
|
2193
|
+
end
|
2194
|
+
|
2195
|
+
class SchemaManipulator
|
2196
|
+
DBINFO_FILE = 'database.yaml'
|
2197
|
+
PERMISSIONS_FILE = 'permissions.yaml'
|
2198
|
+
ACCESS_SUBDIR = 'access'
|
2199
|
+
INDEXES_SUBDIR = 'indexes'
|
2200
|
+
STRUCTURE_SUBDIR = 'structure'
|
2201
|
+
VERINC_FILE = 'branch-upgrade.yaml'
|
2202
|
+
|
2203
|
+
def initialize(path)
|
2204
|
+
@path = Pathname.new(path)
|
2205
|
+
@db_info = YAML.load_file(@path + DBINFO_FILE)
|
2206
|
+
raise TypeError, "Expected Hash in #{DBINFO_FILE}" unless Hash === @db_info
|
2207
|
+
@db_info = Hash.new do |h, k|
|
2208
|
+
raise Error, "#{DBINFO_FILE} missing key #{k.inspect}"
|
2209
|
+
end.update(@db_info)
|
2210
|
+
|
2211
|
+
extend(@db_specifics = case @db_info['system']
|
2212
|
+
when 'Microsoft SQL Server' then MSSQLSpecifics
|
2213
|
+
else NoSpecifics
|
2214
|
+
end)
|
2215
|
+
|
2216
|
+
extend(@vcs_specifics = [
|
2217
|
+
SubversionSpecifics,
|
2218
|
+
GitSpecifics,
|
2219
|
+
].find {|s| s.manages(path)} || NoSpecifics)
|
2220
|
+
end
|
2221
|
+
|
2222
|
+
attr_reader :path
|
2223
|
+
|
2224
|
+
def branch_upgrade_file
|
2225
|
+
@path.join(STRUCTURE_SUBDIR, VERINC_FILE)
|
2226
|
+
end
|
2227
|
+
end
|
2228
|
+
|
2229
|
+
class SchemaUpdater < SchemaManipulator
|
2230
|
+
DEV_SCRIPT_WARNING = <<-"END_OF_TEXT"
|
2231
|
+
*********************************************************
|
2232
|
+
*** WARNING ***
|
2233
|
+
*********************************************************
|
2234
|
+
|
2235
|
+
THIS SCRIPT IS FOR USE ONLY ON DEVELOPMENT DATABASES.
|
2236
|
+
|
2237
|
+
IF RUN ON AN EMPTY DATABASE IT WILL CREATE A DEVELOPMENT
|
2238
|
+
DATABASE THAT IS NOT GUARANTEED TO FOLLOW ANY COMMITTED
|
2239
|
+
MIGRATION PATH.
|
2240
|
+
|
2241
|
+
RUNNING THIS SCRIPT ON A PRODUCTION DATABASE WILL FAIL.
|
2242
|
+
END_OF_TEXT
|
2243
|
+
|
2244
|
+
def initialize(path)
|
2245
|
+
super(path)
|
2246
|
+
|
2247
|
+
@file_based_groups = []
|
2248
|
+
|
2249
|
+
begin
|
2250
|
+
@file_based_groups << (@access_artifacts = AccessArtifactCollection.new(
|
2251
|
+
@path.join(ACCESS_SUBDIR),
|
2252
|
+
:db_specifics=>@db_specifics,
|
2253
|
+
:filename_metavariable=>@db_info.fetch('filename metavariable', nil)
|
2254
|
+
))
|
2255
|
+
@file_based_groups << (@indexes = IndexCollection.new(
|
2256
|
+
@path.join(INDEXES_SUBDIR),
|
2257
|
+
:db_specifics=>@db_specifics
|
2258
|
+
))
|
2259
|
+
@file_based_groups << (@migrations = MigrationChain.new(
|
2260
|
+
@path.join(STRUCTURE_SUBDIR),
|
2261
|
+
:db_specifics=>@db_specifics
|
2262
|
+
))
|
2263
|
+
|
2264
|
+
@branch_upgrade = BranchUpgrade.new(branch_upgrade_file)
|
2265
|
+
@file_based_groups << [@branch_upgrade] if @branch_upgrade.found?
|
2266
|
+
rescue Error
|
2267
|
+
raise
|
2268
|
+
rescue StandardError
|
2269
|
+
raise Error, "Error initializing #{self.class} components"
|
2270
|
+
end
|
2271
|
+
|
2272
|
+
@production = false
|
2273
|
+
end
|
2274
|
+
|
2275
|
+
attr_accessor :production
|
2276
|
+
attr_reader :migrations, :access_artifacts, :indexes, :branch_upgrade
|
2277
|
+
|
2278
|
+
def inspect
|
2279
|
+
"<#{self.class.name}: path=#{path.to_s.inspect}, db=#{@db_specifics}, vcs=#{@vcs_specifics}>"
|
2280
|
+
end
|
2281
|
+
|
2282
|
+
def in_ddl_transaction
|
2283
|
+
yield
|
2284
|
+
end
|
2285
|
+
|
2286
|
+
def ddl_block_separator; "\n"; end
|
2287
|
+
|
2288
|
+
def update_sql
|
2289
|
+
raise XMigra::Error, "Incomplete migration chain" unless @migrations.complete?
|
2290
|
+
raise XMigra::Error, "Unchained migrations exist" unless @migrations.includes_all?
|
2291
|
+
if respond_to? :warning
|
2292
|
+
@branch_upgrade.warnings.each {|w| warning(w)}
|
2293
|
+
if @branch_upgrade.found? && !@branch_upgrade.applicable?(@migrations)
|
2294
|
+
warning("#{branch_upgrade.file_path} does not apply to the current migration chain.")
|
2295
|
+
end
|
2296
|
+
end
|
2297
|
+
|
2298
|
+
check_working_copy!
|
2299
|
+
|
2300
|
+
intro_comment = @db_info.fetch('script comment', '')
|
2301
|
+
intro_comment << if production
|
2302
|
+
sql_comment_block(vcs_information || "")
|
2303
|
+
else
|
2304
|
+
sql_comment_block(DEV_SCRIPT_WARNING)
|
2305
|
+
end
|
2306
|
+
intro_comment << "\n\n"
|
2307
|
+
|
2308
|
+
# If supported, wrap transactionality around modifications
|
2309
|
+
intro_comment + in_ddl_transaction do
|
2310
|
+
script_parts = [
|
2311
|
+
# Check for blatantly incorrect application of script, e.g. running
|
2312
|
+
# on master or template database.
|
2313
|
+
:check_execution_environment_sql,
|
2314
|
+
|
2315
|
+
# Create schema version control (SVC) tables if they don't exist
|
2316
|
+
:ensure_version_tables_sql,
|
2317
|
+
|
2318
|
+
# Create and fill a temporary table with migration IDs known by
|
2319
|
+
# the script with order information
|
2320
|
+
:create_and_fill_migration_table_sql,
|
2321
|
+
|
2322
|
+
# Create and fill a temporary table with index information known by
|
2323
|
+
# the script
|
2324
|
+
:create_and_fill_indexes_table_sql,
|
2325
|
+
|
2326
|
+
# Check that all migrations applied to the database are known to
|
2327
|
+
# the script (as far back as the most recent "version bridge" record)
|
2328
|
+
:check_preceding_migrations_sql,
|
2329
|
+
|
2330
|
+
# Check that there are no "gaps" in the chain of migrations
|
2331
|
+
# that have already been applied
|
2332
|
+
:check_chain_continuity_sql,
|
2333
|
+
|
2334
|
+
# Mark migrations in the temporary table that should be installed
|
2335
|
+
:select_for_install_sql,
|
2336
|
+
|
2337
|
+
# Check production configuration of database
|
2338
|
+
:production_config_check_sql,
|
2339
|
+
|
2340
|
+
# Remove all access artifacts
|
2341
|
+
:remove_access_artifacts_sql,
|
2342
|
+
|
2343
|
+
# Remove all undesired indexes
|
2344
|
+
:remove_undesired_indexes_sql,
|
2345
|
+
|
2346
|
+
# Apply a branch upgrade if indicated
|
2347
|
+
:branch_upgrade_sql,
|
2348
|
+
|
2349
|
+
# Apply selected migrations
|
2350
|
+
:apply_migration_sql,
|
2351
|
+
|
2352
|
+
# Create all access artifacts
|
2353
|
+
:create_access_artifacts_sql,
|
2354
|
+
|
2355
|
+
# Create any desired indexes that don't yet exist
|
2356
|
+
:create_new_indexes_sql,
|
2357
|
+
|
2358
|
+
# Any cleanup needed
|
2359
|
+
:upgrade_cleanup_sql,
|
2360
|
+
]
|
2361
|
+
|
2362
|
+
amend_script_parts(script_parts)
|
2363
|
+
|
2364
|
+
script_parts.map {|mn| self.send(mn)}.flatten.compact.join(ddl_block_separator)
|
2365
|
+
end
|
2366
|
+
end
|
2367
|
+
|
2368
|
+
def amend_script_parts(parts)
|
2369
|
+
end
|
2370
|
+
|
2371
|
+
def sql_comment_block(text)
|
2372
|
+
text.lines.collect {|l| '-- ' + l.chomp + "\n"}.join('')
|
2373
|
+
end
|
2374
|
+
|
2375
|
+
def check_working_copy!
|
2376
|
+
raise VersionControlError, "XMigra source not under version control" if production
|
2377
|
+
end
|
2378
|
+
|
2379
|
+
def create_access_artifacts_sql
|
2380
|
+
scripts = []
|
2381
|
+
@access_artifacts.each_definition_sql {|s| scripts << s}
|
2382
|
+
return scripts unless scripts.empty?
|
2383
|
+
end
|
2384
|
+
|
2385
|
+
def apply_migration_sql
|
2386
|
+
# Apply selected migrations
|
2387
|
+
@migrations.collect do |m|
|
2388
|
+
m.migration_application_sql
|
2389
|
+
end
|
2390
|
+
end
|
2391
|
+
|
2392
|
+
def branch_upgrade_sql
|
2393
|
+
end
|
2394
|
+
|
2395
|
+
def upgrade_cleanup_sql
|
2396
|
+
end
|
2397
|
+
|
2398
|
+
def vcs_information
|
2399
|
+
end
|
2400
|
+
|
2401
|
+
def each_file_path
|
2402
|
+
@file_based_groups.each do |group|
|
2403
|
+
group.each {|item| yield item.file_path}
|
2404
|
+
end
|
2405
|
+
end
|
2406
|
+
end
|
2407
|
+
|
2408
|
+
class NewMigrationAdder < SchemaManipulator
|
2409
|
+
OBSOLETE_VERINC_FILE = 'version-upgrade-obsolete.yaml'
|
2410
|
+
|
2411
|
+
def initialize(path)
|
2412
|
+
super(path)
|
2413
|
+
end
|
2414
|
+
|
2415
|
+
def add_migration(summary, options={})
|
2416
|
+
struct_dir = @path.join(STRUCTURE_SUBDIR)
|
2417
|
+
FileUtils.mkdir_p(struct_dir) unless struct_dir.exist?
|
2418
|
+
|
2419
|
+
# Load the head YAML from the structure subdir if it exists or create
|
2420
|
+
# default empty migration chain
|
2421
|
+
head_file = struct_dir.join(MigrationChain::HEAD_FILE)
|
2422
|
+
head_info = if head_file.exist?
|
2423
|
+
YAML.parse_file(head_file).transform
|
2424
|
+
else
|
2425
|
+
{}
|
2426
|
+
end
|
2427
|
+
Hash === head_info or raise XMigra::Error, "Invalid #{MigrationChain::HEAD_FILE} format"
|
2428
|
+
|
2429
|
+
new_fpath = struct_dir.join(
|
2430
|
+
[Date.today.strftime("%Y-%m-%d"), summary].join(' ') + '.yaml'
|
2431
|
+
)
|
2432
|
+
raise(XMigra::Error, "Migration file\"#{new_fpath.basename}\" already exists") if new_fpath.exist?
|
2433
|
+
|
2434
|
+
new_data = {
|
2435
|
+
Migration::FOLLOWS=>head_info.fetch(MigrationChain::LATEST_CHANGE, Migration::EMPTY_DB),
|
2436
|
+
'sql'=>options.fetch(:sql, "<<<<< INSERT SQL HERE >>>>>\n"),
|
2437
|
+
'description'=>options.fetch(:description, "<<<<< DESCRIPTION OF MIGRATION >>>>>").dup.extend(FoldedYamlStyle),
|
2438
|
+
Migration::CHANGES=>options.fetch(:changes, ["<<<<< WHAT THIS MIGRATION CHANGES >>>>>"]),
|
2439
|
+
}
|
2440
|
+
|
2441
|
+
# Write the head file first, in case a lock is required
|
2442
|
+
old_head_info = head_info.dup
|
2443
|
+
head_info[MigrationChain::LATEST_CHANGE] = new_fpath.basename('.yaml').to_s
|
2444
|
+
File.open(head_file, "w") do |f|
|
2445
|
+
$xmigra_yamler.dump(head_info, f)
|
2446
|
+
end
|
2447
|
+
|
2448
|
+
begin
|
2449
|
+
File.open(new_fpath, "w") do |f|
|
2450
|
+
$xmigra_yamler.dump(new_data, f)
|
2451
|
+
end
|
2452
|
+
rescue
|
2453
|
+
# Revert the head file to it's previous state
|
2454
|
+
File.open(head_file, "w") do |f|
|
2455
|
+
$xmigra_yamler.dump(old_head_info, f)
|
2456
|
+
end
|
2457
|
+
|
2458
|
+
raise
|
2459
|
+
end
|
2460
|
+
|
2461
|
+
# Obsolete any existing branch upgrade file
|
2462
|
+
bufp = branch_upgrade_file
|
2463
|
+
if bufp.exist?
|
2464
|
+
warning("#{bufp.relative_path_from(@path)} is obsolete and will be renamed.") if respond_to? :warning
|
2465
|
+
|
2466
|
+
obufp = bufp.dirname.join(OBSOLETE_VERINC_FILE)
|
2467
|
+
rm_method = respond_to?(:vcs_remove) ? method(:vcs_remove) : FileUtils.method(:rm)
|
2468
|
+
mv_method = respond_to?(:vcs_move) ? method(:vcs_move) : FileUtils.method(:mv)
|
2469
|
+
|
2470
|
+
rm_method.call(obufp) if obufp.exist?
|
2471
|
+
mv_method.call(bufp, obufp)
|
2472
|
+
end
|
2473
|
+
|
2474
|
+
return new_fpath
|
2475
|
+
end
|
2476
|
+
end
|
2477
|
+
|
2478
|
+
class PermissionScriptWriter < SchemaManipulator
|
2479
|
+
def initialize(path)
|
2480
|
+
super(path)
|
2481
|
+
|
2482
|
+
@permissions = YAML.load_file(self.path + PERMISSIONS_FILE)
|
2483
|
+
raise TypeError, "Expected Hash in #{PERMISSIONS_FILE}" unless Hash === @permissions
|
2484
|
+
end
|
2485
|
+
|
2486
|
+
def in_ddl_transaction
|
2487
|
+
yield
|
2488
|
+
end
|
2489
|
+
|
2490
|
+
def ddl_block_separator; "\n"; end
|
2491
|
+
|
2492
|
+
def permissions_sql
|
2493
|
+
intro_comment = @db_info.fetch('script comment', '') + "\n\n"
|
2494
|
+
|
2495
|
+
intro_comment + in_ddl_transaction do
|
2496
|
+
[
|
2497
|
+
# Check for blatantly incorrect application of script, e.g. running
|
2498
|
+
# on master or template database.
|
2499
|
+
check_execution_environment_sql,
|
2500
|
+
|
2501
|
+
# Create table for recording granted permissions if it doesn't exist
|
2502
|
+
ensure_permissions_table_sql,
|
2503
|
+
|
2504
|
+
# Revoke permissions previously granted through an XMigra permissions
|
2505
|
+
# script
|
2506
|
+
revoke_previous_permissions_sql,
|
2507
|
+
|
2508
|
+
# Grant the permissions indicated in the source file
|
2509
|
+
grant_specified_permissions_sql,
|
2510
|
+
|
2511
|
+
].flatten.compact.join(ddl_block_separator)
|
2512
|
+
end
|
2513
|
+
end
|
2514
|
+
|
2515
|
+
def grant_specified_permissions_sql
|
2516
|
+
granting_permissions_comment_sql +
|
2517
|
+
enum_for(:each_specified_grant).map(&method(:grant_permissions_sql)).join("\n")
|
2518
|
+
end
|
2519
|
+
|
2520
|
+
def each_specified_grant
|
2521
|
+
@permissions.each_pair do |object, grants|
|
2522
|
+
grants.each_pair do |principal, permissions|
|
2523
|
+
permissions = [permissions] unless permissions.is_a? Enumerable
|
2524
|
+
yield permissions, object, principal
|
2525
|
+
end
|
2526
|
+
end
|
2527
|
+
end
|
2528
|
+
|
2529
|
+
def line_comment(contents)
|
2530
|
+
"-- " + contents + " --\n"
|
2531
|
+
end
|
2532
|
+
|
2533
|
+
def header(content, size)
|
2534
|
+
dashes = size - content.length - 2
|
2535
|
+
l_dashes = dashes / 2
|
2536
|
+
r_dashes = dashes - l_dashes
|
2537
|
+
('-' * l_dashes) + ' ' + content + ' ' + ('-' * r_dashes)
|
2538
|
+
end
|
2539
|
+
end
|
2540
|
+
|
2541
|
+
module WarnToStderr
|
2542
|
+
def warning(message)
|
2543
|
+
STDERR.puts("Warning: " + message)
|
2544
|
+
STDERR.puts
|
2545
|
+
end
|
2546
|
+
end
|
2547
|
+
|
2548
|
+
module FoldedYamlStyle
|
2549
|
+
def to_yaml_style
|
2550
|
+
:fold
|
2551
|
+
end
|
2552
|
+
|
2553
|
+
if defined? Psych
|
2554
|
+
def yaml_style
|
2555
|
+
Psych::Nodes::Scalar::FOLDED
|
2556
|
+
end
|
2557
|
+
end
|
2558
|
+
|
2559
|
+
end
|
2560
|
+
|
2561
|
+
class Program
|
2562
|
+
ILLEGAL_PATH_CHARS = "\"<>:|"
|
2563
|
+
ILLEGAL_FILENAME_CHARS = ILLEGAL_PATH_CHARS + "/\\"
|
2564
|
+
|
2565
|
+
class TerminatingOption < Exception; end
|
2566
|
+
class ArgumentError < XMigra::Error; end
|
2567
|
+
module QuietError; end
|
2568
|
+
|
2569
|
+
class << self
|
2570
|
+
def subcommand(name, description, &block)
|
2571
|
+
(@subcommands ||= {})[name] = block
|
2572
|
+
(@subcommand_descriptions ||= {})[name] = description
|
2573
|
+
end
|
2574
|
+
|
2575
|
+
# Run the given command line.
|
2576
|
+
#
|
2577
|
+
# An array of command line arguments may be given as the only argument
|
2578
|
+
# or arguments may be given as call parameters. Returns nil if the
|
2579
|
+
# command completed or a TerminatingOption object if a terminating
|
2580
|
+
# option (typically "--help") was passed.
|
2581
|
+
def run(*argv)
|
2582
|
+
options = (Hash === argv.last) ? argv.pop : {}
|
2583
|
+
argv = argv[0] if argv.length == 1 && Array === argv[0]
|
2584
|
+
prev_subcommand = @active_subcommand
|
2585
|
+
begin
|
2586
|
+
@active_subcommand = subcmd = argv.shift
|
2587
|
+
|
2588
|
+
begin
|
2589
|
+
if subcmd == "help" || subcmd.nil?
|
2590
|
+
help(argv)
|
2591
|
+
return
|
2592
|
+
end
|
2593
|
+
|
2594
|
+
begin
|
2595
|
+
(@subcommands[subcmd] || method(:show_subcommands_as_help)).call(argv)
|
2596
|
+
rescue StandardError => error
|
2597
|
+
raise unless options[:error]
|
2598
|
+
options[:error].call(error)
|
2599
|
+
end
|
2600
|
+
rescue TerminatingOption => stop
|
2601
|
+
return stop
|
2602
|
+
end
|
2603
|
+
ensure
|
2604
|
+
@active_subcommand = prev_subcommand
|
2605
|
+
end
|
2606
|
+
end
|
2607
|
+
|
2608
|
+
def help(argv)
|
2609
|
+
if (argv.length != 1) || (argv[0] == '--help')
|
2610
|
+
show_subcommands
|
2611
|
+
return
|
2612
|
+
end
|
2613
|
+
|
2614
|
+
argv << "--help"
|
2615
|
+
run(argv)
|
2616
|
+
end
|
2617
|
+
|
2618
|
+
def show_subcommands(_1=nil)
|
2619
|
+
puts
|
2620
|
+
puts "Use '#{File.basename($0)} help <subcommand>' for help on one of these subcommands:"
|
2621
|
+
puts
|
2622
|
+
|
2623
|
+
descs = @subcommand_descriptions
|
2624
|
+
cmd_width = descs.enum_for(:each_key).max_by {|i| i.length}.length + 2
|
2625
|
+
descs.each_pair do |cmd, description|
|
2626
|
+
printf("%*s - ", cmd_width, cmd)
|
2627
|
+
description.lines.each_with_index do |line, i|
|
2628
|
+
indent = if (i > 0)..(i == description.lines.count - 1)
|
2629
|
+
cmd_width + 3
|
2630
|
+
else
|
2631
|
+
0
|
2632
|
+
end
|
2633
|
+
puts(" " * indent + line.chomp)
|
2634
|
+
end
|
2635
|
+
end
|
2636
|
+
end
|
2637
|
+
|
2638
|
+
def show_subcommands_as_help(_1=nil)
|
2639
|
+
show_subcommands
|
2640
|
+
raise ArgumentError.new("Invalid subcommand").extend(QuietError)
|
2641
|
+
end
|
2642
|
+
|
2643
|
+
def command_line(argv, use, cmdopts = {})
|
2644
|
+
options = OpenStruct.new
|
2645
|
+
argument_desc = cmdopts[:argument_desc]
|
2646
|
+
|
2647
|
+
optparser = OptionParser.new do |flags|
|
2648
|
+
subcmd = @active_subcommand || "<subcmd>"
|
2649
|
+
flags.banner = [
|
2650
|
+
"Usage: #{File.basename($0)} #{subcmd} [<options>]",
|
2651
|
+
argument_desc
|
2652
|
+
].compact.join(' ')
|
2653
|
+
flags.banner << "\n\n" + cmdopts[:help].chomp if cmdopts[:help]
|
2654
|
+
|
2655
|
+
flags.separator ''
|
2656
|
+
flags.separator 'Subcommand options:'
|
2657
|
+
|
2658
|
+
if use[:target_type]
|
2659
|
+
options.target_type = :unspecified
|
2660
|
+
allowed = [:exact, :substring, :regexp]
|
2661
|
+
flags.on(
|
2662
|
+
"--by=TYPE", allowed,
|
2663
|
+
"Specify how TARGETs are matched",
|
2664
|
+
"against subject strings",
|
2665
|
+
"(#{allowed.collect {|i| i.to_s}.join(', ')})"
|
2666
|
+
) do |type|
|
2667
|
+
options.target_type = type
|
2668
|
+
end
|
2669
|
+
end
|
2670
|
+
|
2671
|
+
if use[:dev_branch]
|
2672
|
+
options.dev_branch = false
|
2673
|
+
flags.on("--dev-branch", "Favor development branch usage assumption") do
|
2674
|
+
options.dev_branch = true
|
2675
|
+
end
|
2676
|
+
end
|
2677
|
+
|
2678
|
+
unless use[:edit].nil?
|
2679
|
+
options.edit = use[:edit] ? true : false
|
2680
|
+
flags.banner << "\n\n" << (<<END_OF_HELP).chomp
|
2681
|
+
When opening an editor, the program specified by the environment variable
|
2682
|
+
VISUAL is preferred, then the one specified by EDITOR. If neither of these
|
2683
|
+
environment variables is set no editor will be opened.
|
2684
|
+
END_OF_HELP
|
2685
|
+
flags.on("--[no-]edit", "Open the resulting file in an editor",
|
2686
|
+
"(defaults to #{options.edit})") do |v|
|
2687
|
+
options.edit = %w{EDITOR VISUAL}.any? {|k| ENV.has_key?(k)} && v
|
2688
|
+
end
|
2689
|
+
end
|
2690
|
+
|
2691
|
+
if use[:search_type]
|
2692
|
+
options.search_type = :changes
|
2693
|
+
allowed = [:changes, :sql]
|
2694
|
+
flags.on(
|
2695
|
+
"--match=SUBJECT", allowed,
|
2696
|
+
"Specify the type of subject against",
|
2697
|
+
"which TARGETs match",
|
2698
|
+
"(#{allowed.collect {|i| i.to_s}.join(', ')})"
|
2699
|
+
) do |type|
|
2700
|
+
options.search_type = type
|
2701
|
+
end
|
2702
|
+
end
|
2703
|
+
|
2704
|
+
if use[:outfile]
|
2705
|
+
options.outfile = nil
|
2706
|
+
flags.on("-o", "--outfile=FILE", "Output to FILE") do |fpath|
|
2707
|
+
options.outfile = File.expand_path(fpath)
|
2708
|
+
end
|
2709
|
+
end
|
2710
|
+
|
2711
|
+
if use[:production]
|
2712
|
+
options.production = false
|
2713
|
+
flags.on("-p", "--production", "Generate script for production databases") do
|
2714
|
+
options.production = true
|
2715
|
+
end
|
2716
|
+
end
|
2717
|
+
|
2718
|
+
options.source_dir = Dir.pwd
|
2719
|
+
flags.on("--source=DIR", "Work from/on the schema in DIR") do |dir|
|
2720
|
+
options.source_dir = File.expand_path(dir)
|
2721
|
+
end
|
2722
|
+
|
2723
|
+
flags.on_tail("-h", "--help", "Show this message") do
|
2724
|
+
puts
|
2725
|
+
puts flags
|
2726
|
+
raise TerminatingOption.new('--help')
|
2727
|
+
end
|
2728
|
+
end
|
2729
|
+
|
2730
|
+
argv = optparser.parse(argv)
|
2731
|
+
|
2732
|
+
if use[:target_type] && options.target_type == :unspecified
|
2733
|
+
options.target_type = case options.search_type
|
2734
|
+
when :changes then :strict
|
2735
|
+
else :substring
|
2736
|
+
end
|
2737
|
+
end
|
2738
|
+
|
2739
|
+
return argv, options
|
2740
|
+
end
|
2741
|
+
|
2742
|
+
def output_to(fpath_or_nil)
|
2743
|
+
if fpath_or_nil.nil?
|
2744
|
+
yield(STDOUT)
|
2745
|
+
else
|
2746
|
+
File.open(fpath_or_nil, "w") do |stream|
|
2747
|
+
yield(stream)
|
2748
|
+
end
|
2749
|
+
end
|
2750
|
+
end
|
2751
|
+
|
2752
|
+
def argument_error_unless(test, message)
|
2753
|
+
return if test
|
2754
|
+
raise ArgumentError, XMigra.program_message(message, :cmd=>@active_subcommand)
|
2755
|
+
end
|
2756
|
+
|
2757
|
+
def edit(fpath)
|
2758
|
+
case
|
2759
|
+
when (editor = ENV['VISUAL']) && PLATFORM == :mswin
|
2760
|
+
system(%Q{start #{editor} "#{fpath}"})
|
2761
|
+
when editor = ENV['VISUAL']
|
2762
|
+
system(%Q{#{editor} "#{fpath}" &})
|
2763
|
+
when editor = ENV['EDITOR']
|
2764
|
+
system(%Q{#{editor} "#{fpath}"})
|
2765
|
+
end
|
2766
|
+
end
|
2767
|
+
end
|
2768
|
+
|
2769
|
+
subcommand 'overview', "Explain usage of this tool" do |argv|
|
2770
|
+
argument_error_unless([[], ["-h"], ["--help"]].include?(argv),
|
2771
|
+
"'%prog %cmd' does not accept arguments.")
|
2772
|
+
|
2773
|
+
formalizations = {
|
2774
|
+
/xmigra/i=>'XMigra',
|
2775
|
+
}
|
2776
|
+
|
2777
|
+
section = proc do |name, content|
|
2778
|
+
puts
|
2779
|
+
puts name
|
2780
|
+
puts "=" * name.length
|
2781
|
+
puts XMigra.program_message(
|
2782
|
+
content,
|
2783
|
+
:prog=>/%program_cmd\b/
|
2784
|
+
)
|
2785
|
+
end
|
2786
|
+
|
2787
|
+
puts XMigra.program_message(<<END_HEADER) # Overview
|
2788
|
+
|
2789
|
+
===========================================================================
|
2790
|
+
# Usage of %program_name
|
2791
|
+
===========================================================================
|
2792
|
+
END_HEADER
|
2793
|
+
|
2794
|
+
begin; section['Introduction', <<END_SECTION]
|
2795
|
+
|
2796
|
+
%program_name is a tool designed to assist development of software using
|
2797
|
+
relational databases for persistent storage. During the development cycle, this
|
2798
|
+
tool helps manage:
|
2799
|
+
|
2800
|
+
- Migration of production databases to newer versions, including migration
|
2801
|
+
between parallel, released versions.
|
2802
|
+
|
2803
|
+
- Using transactional scripts, so that unexpected database conditions do not
|
2804
|
+
lead to corrupt production databases.
|
2805
|
+
|
2806
|
+
- Protection of production databases from changes still under development.
|
2807
|
+
|
2808
|
+
- Parallel development of features requiring database changes.
|
2809
|
+
|
2810
|
+
- Assignment of permissions to database objects.
|
2811
|
+
|
2812
|
+
To accomplish this, the database schema to be created is decomposed into
|
2813
|
+
several parts and formatted in text files according to certain rules. The
|
2814
|
+
%program_name tool is then used to manipulate, query, or generate scripts from
|
2815
|
+
the set of files.
|
2816
|
+
END_SECTION
|
2817
|
+
end
|
2818
|
+
begin; section['Schema Files and Folders', <<END_SECTION]
|
2819
|
+
|
2820
|
+
SCHEMA (root folder/directory of decomposed schema)
|
2821
|
+
+-- database.yaml
|
2822
|
+
+-- permissions.yaml (optional)
|
2823
|
+
+-- structure
|
2824
|
+
| +-- head.yaml
|
2825
|
+
| +-- <migration files>
|
2826
|
+
| ...
|
2827
|
+
+-- access
|
2828
|
+
| +-- <stored procedure definition files>
|
2829
|
+
| +-- <view definition files>
|
2830
|
+
| +-- <user defined function definition files>
|
2831
|
+
| ...
|
2832
|
+
+-- indexes
|
2833
|
+
+-- <index definition files>
|
2834
|
+
...
|
2835
|
+
|
2836
|
+
--------------------------------------------------------------------------
|
2837
|
+
NOTE: In case-sensitive filesystems, all file and directory names dictated
|
2838
|
+
by %program_name are lowercase.
|
2839
|
+
--------------------------------------------------------------------------
|
2840
|
+
|
2841
|
+
All data files used by %program_name conform to the YAML 1.0 data format
|
2842
|
+
specification. Please refer to that specification for information
|
2843
|
+
on the specifics of encoding particular values. This documentation, at many
|
2844
|
+
points, makes reference to "sections" of a .yaml file; such a section is,
|
2845
|
+
technically, an entry in the mapping at the top level of the .yaml file with
|
2846
|
+
the given key. The simplest understanding of this is that the section name
|
2847
|
+
followed immediately (no whitespace) by a colon (':') and at least one space
|
2848
|
+
character appears in the left-most column, and the section contents appear
|
2849
|
+
either on the line after the colon-space or in an indented block starting on
|
2850
|
+
the next line (often used with a scalar block indicator ('|' or '>') following
|
2851
|
+
the colon-space).
|
2852
|
+
|
2853
|
+
The decomposed database schema lives within a filesystem subtree rooted at a
|
2854
|
+
single folder (i.e. directory). For examples in this documentation, that
|
2855
|
+
folder will be called SCHEMA. Two important files are stored directly in the
|
2856
|
+
SCHEMA directory: database.yaml and permissions.yaml. The "database.yaml" file
|
2857
|
+
provides general information about the database for which scripts are to be
|
2858
|
+
generated. Please see the section below detailing this file's contents for
|
2859
|
+
more information. The "permissions.yaml" file specifies permissions to be
|
2860
|
+
granted when generating a permission-granting script (run
|
2861
|
+
'%program_cmd help permissions' for more information).
|
2862
|
+
|
2863
|
+
Within the SCHEMA folder, %program_name expects three other folders: structure,
|
2864
|
+
access, and indexes.
|
2865
|
+
END_SECTION
|
2866
|
+
end
|
2867
|
+
begin; section['The "SCHEMA/structure" Folder', <<END_SECTION]
|
2868
|
+
|
2869
|
+
Every relational database has structures in which it stores the persistent
|
2870
|
+
data of the related application(s). These database objects are special in
|
2871
|
+
relation to other parts of the database because they contain information that
|
2872
|
+
cannot be reproduced just from the schema definition. Yet bug fixes and
|
2873
|
+
feature additions will need to update this structure and good programming
|
2874
|
+
practice dictates that such changes, and the functionalities relying on them,
|
2875
|
+
need to be tested. Testability, in turn, dictates a repeatable sequence of
|
2876
|
+
actions to be executed on the database starting from a known state.
|
2877
|
+
|
2878
|
+
%program_name models the evolution of the persistent data storage structures
|
2879
|
+
of a database as a chain of "migrations," each of which makes changes to the
|
2880
|
+
database storage structure from a previous, known state of the database. The
|
2881
|
+
first migration starts at an empty database and each subsequent migration
|
2882
|
+
starts where the previous migration left off. Each migration is stored in
|
2883
|
+
a file within the SCHEMA/structure folder. The names of migration files start
|
2884
|
+
with a date (YYYY-MM-DD) and include a short description of the change. As
|
2885
|
+
with other files used by the %program_name tool, migration files are in the
|
2886
|
+
YAML format, using the ".yaml" extension. Because some set of migrations
|
2887
|
+
will determine the state of production databases, migrations themselves (as
|
2888
|
+
used to produce production upgrade scripts) must be "set in stone" -- once
|
2889
|
+
committed to version control (on a production branch) they must never change
|
2890
|
+
their content.
|
2891
|
+
|
2892
|
+
Migration files are usually generated by running the '%program_cmd new'
|
2893
|
+
command (see '%program_cmd help new' for more information) and then editing
|
2894
|
+
the resulting file. The migration file has several sections: "starting from",
|
2895
|
+
"sql", "changes", and "description". The "starting from" section indicates the
|
2896
|
+
previous migration in the chain (or "empty database" for the first migration).
|
2897
|
+
SQL code that effects the migration on the database is the content of the "sql"
|
2898
|
+
section (usually given as a YAML literal block). The "changes" section
|
2899
|
+
supports '%program_cmd history', allowing a more user-friendly look at the
|
2900
|
+
evolution of a subset of the database structure over the migration chain.
|
2901
|
+
Finally, the "description" section is intended for a prose description of the
|
2902
|
+
migration, and is included in the upgrade metadata stored in the database. Use
|
2903
|
+
of the '%program_cmd new' command is recommended; it handles several tiresome
|
2904
|
+
and error-prone tasks: creating a new migration file with a conformant name,
|
2905
|
+
setting the "starting from" section to the correct value, and updating
|
2906
|
+
SCHEMA/structure/head.yaml to reference the newly generated file.
|
2907
|
+
|
2908
|
+
The SCHEMA/structure/head.yaml file deserves special note: it contains a
|
2909
|
+
reference to the last migration to be applied. Because of this, parallel
|
2910
|
+
development of database changes will cause conflicts in the contents of this
|
2911
|
+
file. This is by design, and '%program_cmd unbranch' will assist in resolving
|
2912
|
+
these conflicts.
|
2913
|
+
|
2914
|
+
Care must be taken when committing migration files to version control; because
|
2915
|
+
the structure of production databases will be determined by the chain of
|
2916
|
+
migrations (starting at an empty database, going up to some certain point),
|
2917
|
+
it is imperative that migrations used to build these production upgrade scripts
|
2918
|
+
not be modified once committed to the version control system. When building
|
2919
|
+
a production upgrade script, %program_name verifies that this constraint is
|
2920
|
+
followed. Therefore, if the need arises to commit a migration file that may
|
2921
|
+
require amendment, the best practice is to commit it to a development branch.
|
2922
|
+
|
2923
|
+
Migrating a database from one released version (which may receive bug fixes
|
2924
|
+
or critical feature updates) to another released version which developed along
|
2925
|
+
a parallel track is generally a tricky endeavor. Please see the section on
|
2926
|
+
"branch upgrades" below for information on how %program_name supports this
|
2927
|
+
use case.
|
2928
|
+
END_SECTION
|
2929
|
+
end
|
2930
|
+
begin; section['The "SCHEMA/access" Folder', <<END_SECTION]
|
2931
|
+
|
2932
|
+
In addition to the structures that store persistent data, many relational
|
2933
|
+
databases also support persistent constructs for providing consistent access
|
2934
|
+
(creation, retrieval, update, and deletion) to the persistent data even as the
|
2935
|
+
actual storage structure changes, allowing for a degree of backward
|
2936
|
+
compatibility with applications. These constructs do not, of themselves,
|
2937
|
+
contain persistent data, but rather specify procedures for accessing the
|
2938
|
+
persistent data.
|
2939
|
+
|
2940
|
+
In %program_name, such constructs are defined in the SCHEMA/access folder, with
|
2941
|
+
each construct (usually) having its own file. The name of the file must be
|
2942
|
+
a valid SQL name for the construct defined. The filename may be accessed
|
2943
|
+
within the definition by the filename metavariable, by default "[{filename}]"
|
2944
|
+
(without quotation marks); this assists renaming such constructs, making the
|
2945
|
+
operation of renaming the construct a simple rename of the containing file
|
2946
|
+
within the filesystem (and version control repository). Use of files in this
|
2947
|
+
way creates a history of each "access object's" definition in the version
|
2948
|
+
control system organized by the name of the object.
|
2949
|
+
|
2950
|
+
The definition of the access object is given in the "sql" section of the
|
2951
|
+
definition file, usually with a YAML literal block. This SQL MUST define the
|
2952
|
+
object for which the containing file is named; failure to do so will result in
|
2953
|
+
failure of the script when it is run against the database. After deleting
|
2954
|
+
all access objects previously created by %program_name, the generated script
|
2955
|
+
first checks that the access object does not exist, then runs the definition
|
2956
|
+
SQL, and finally checks that the object now exists.
|
2957
|
+
|
2958
|
+
In addition to the SQL definition, %program_name needs to know what kind of
|
2959
|
+
object is to be created by this definition. This information is presented in
|
2960
|
+
the "define" section, and is currently limited to "function",
|
2961
|
+
"stored procedure", and "view".
|
2962
|
+
|
2963
|
+
Some database management systems enforce a rule that statements defining access
|
2964
|
+
objects (or at least, some kinds of access objects) may not reference access
|
2965
|
+
objects that do not yet exist. (A good example is Microsoft SQL Server's rule
|
2966
|
+
about user defined functions that means a definition for the function A may
|
2967
|
+
only call the user defined function B if B exists when A is defined.) To
|
2968
|
+
accommodate this situation, %program_name provides an optional "referencing"
|
2969
|
+
section in the access object definition file. The content of this section
|
2970
|
+
must be a YAML sequence of scalars, each of which is the name of an access
|
2971
|
+
object file (the name must be given the same way the filename is written, not
|
2972
|
+
just a way that is equivalent in the target SQL language). The scalar values
|
2973
|
+
must be appropriately escaped as necessary (e.g. Microsoft SQL Server uses
|
2974
|
+
square brackets as a quotation mechanism, and square brackets have special
|
2975
|
+
meaning in YAML, so it is necessary use quoted strings or a scalar block to
|
2976
|
+
contain them). Any access objects listed in this way will be created before
|
2977
|
+
the referencing object.
|
2978
|
+
END_SECTION
|
2979
|
+
end
|
2980
|
+
begin; section['The "SCHEMA/indexes" Folder', <<END_SECTION]
|
2981
|
+
|
2982
|
+
Database indexes vary from the other kinds of definitions supported by
|
2983
|
+
%program_name: while SCHEMA/structure elements only hold data and
|
2984
|
+
SCHEMA/access elements are defined entirely by their code in the schema and
|
2985
|
+
can thus be efficiently re-created, indexes have their whole definition in
|
2986
|
+
the schema, but store data gleaned from the persistent data. Re-creation of
|
2987
|
+
an index is an expensive operation that should be avoided when unnecessary.
|
2988
|
+
|
2989
|
+
To accomplish this end, %program_name looks in the SCHEMA/indexes folder for
|
2990
|
+
index definitions. The generated scripts will drop and (re-)create only
|
2991
|
+
indexes whose definitions are changed. %program_name uses a very literal
|
2992
|
+
comparison of the SQL text used to create the index to determine "change;"
|
2993
|
+
even so much as a single whitespace added or removed, even if insignificant to
|
2994
|
+
the database management system, will be enough to cause the index to be dropped
|
2995
|
+
and re-created.
|
2996
|
+
|
2997
|
+
Index definition files use only the "sql" section to provide the SQL definition
|
2998
|
+
of the index. Index definitions do not support use of the filename
|
2999
|
+
metavariable because renaming an index would cause it to be dropped and
|
3000
|
+
re-created.
|
3001
|
+
END_SECTION
|
3002
|
+
end
|
3003
|
+
begin; section['The "SCHEMA/database.yaml" File', <<END_SECTION]
|
3004
|
+
|
3005
|
+
The SCHEMA/database.yaml file consists of several sections that provide general
|
3006
|
+
information about the database schema. The following subsection detail some
|
3007
|
+
contents that may be included in this file.
|
3008
|
+
|
3009
|
+
system
|
3010
|
+
------
|
3011
|
+
|
3012
|
+
The "system" section specifies for %program_name which database management
|
3013
|
+
system shall be targeted for the generation of scripts. Currently the
|
3014
|
+
supported values are:
|
3015
|
+
|
3016
|
+
- Microsoft SQL Server
|
3017
|
+
|
3018
|
+
Each system can also have sub-settings that modify the generated scripts.
|
3019
|
+
|
3020
|
+
Microsoft SQL Server:
|
3021
|
+
The "MSSQL 2005 compatible" setting in SCEMA/database.yaml, if set to
|
3022
|
+
"true", causes INSERT statements to be generated in a more verbose and
|
3023
|
+
SQL Server 2005 compatible manner.
|
3024
|
+
|
3025
|
+
Also, each system may modify in other ways the behavior of the generator or
|
3026
|
+
the interpretation of the definition files:
|
3027
|
+
|
3028
|
+
Microsoft SQL Server:
|
3029
|
+
The SQL in the definition files may use the "GO" metacommand also found in
|
3030
|
+
Microsoft SQL Server Management Studio and sqlcmd.exe. This metacommand
|
3031
|
+
must be on a line by itself where used. It should produce the same results
|
3032
|
+
as it would in MSSMS or sqlcmd.exe, except that the overall script is
|
3033
|
+
transacted.
|
3034
|
+
|
3035
|
+
script comment
|
3036
|
+
--------------
|
3037
|
+
|
3038
|
+
The "script comment" section defines a body of SQL to be inserted at the top
|
3039
|
+
of all generated scripts. This is useful for including copyright information
|
3040
|
+
in the resulting SQL.
|
3041
|
+
|
3042
|
+
filename metavariable
|
3043
|
+
---------------------
|
3044
|
+
|
3045
|
+
The "filename metavariable" section allows the schema to override the filename
|
3046
|
+
metavariable that is used for access object definitions. The default value
|
3047
|
+
is "[{filename}]" (excluding the quotation marks). If that string is required
|
3048
|
+
in one or more access object definitions, this section allows the schema to
|
3049
|
+
dictate another value.
|
3050
|
+
END_SECTION
|
3051
|
+
end
|
3052
|
+
begin; section['Script Generation Modes', <<END_SECTION]
|
3053
|
+
|
3054
|
+
%program_name supports two modes of upgrade script creation: development and
|
3055
|
+
production. (Permission script generation is not constrained to these modes.)
|
3056
|
+
Upgrade script generation defaults to development mode, which does less
|
3057
|
+
work to generate a script and skips tests intended to ensure that the script
|
3058
|
+
could be generated again at some future point from the contents of the version
|
3059
|
+
control repository. The resulting script can only be run on an empty database
|
3060
|
+
or a database that was set up with a development mode script earlier on the
|
3061
|
+
same migration chain; running a development mode script on a database created
|
3062
|
+
with a production script fails by design (preventing a production database from
|
3063
|
+
undergoing a migration that has not been duly recorded in the version control
|
3064
|
+
system). Development scripts have a big warning comment at the beginning of
|
3065
|
+
the script as a reminder that they are not suitable to use on a production
|
3066
|
+
system.
|
3067
|
+
|
3068
|
+
Use of the '--production' flag with the '%program_cmd upgrade' command
|
3069
|
+
enables production mode, which carries out a significant number of
|
3070
|
+
additional checks. These checks serve two purposes: making sure that all
|
3071
|
+
migrations to be applied to a production database are recorded in the version
|
3072
|
+
control system and that all of the definition files in the whole schema
|
3073
|
+
represent a single, coherent point in the version history (i.e. all files are
|
3074
|
+
from the same revision). Where a case arises that a script needs to be
|
3075
|
+
generated that cannot meet these two criteria, it is almost certainly a case
|
3076
|
+
that calls for a development script. There is always the option of creating
|
3077
|
+
a new production branch and committing the %program_name schema files to that
|
3078
|
+
branch if a production script is needed, thus meeting the criteria of the test.
|
3079
|
+
|
3080
|
+
Note that "development mode" and "production mode" are not about the quality
|
3081
|
+
of the scripts generated or the "build mode" of the application that may access
|
3082
|
+
the resulting database, but rather about the designation of the database
|
3083
|
+
to which the generated scripts may be applied. "Production" scripts certainly
|
3084
|
+
should be tested in a non-production environment before they are applied to
|
3085
|
+
a production environment with irreplaceable data. But "development" scripts,
|
3086
|
+
by design, can never be run on production systems (so that the production
|
3087
|
+
systems only move from one well-documented state to another).
|
3088
|
+
END_SECTION
|
3089
|
+
end
|
3090
|
+
begin; section['Branch Upgrades', <<END_SECTION]
|
3091
|
+
|
3092
|
+
Maintaining a single, canonical chain of database schema migrations released to
|
3093
|
+
customers dramatically reduces the amount of complexity inherent in
|
3094
|
+
same-version product upgrades. But expecting development and releases to stay
|
3095
|
+
on a single migration chain is overly optimistic; there are many circumstances
|
3096
|
+
that lead to divergent migration chains across instances of the database. A
|
3097
|
+
bug fix on an already released version or some emergency feature addition for
|
3098
|
+
an enormous profit opportunity are entirely possible, and any database
|
3099
|
+
evolution system that breaks under those requirements will be an intolerable
|
3100
|
+
hinderance.
|
3101
|
+
|
3102
|
+
%program_name supports these situations through the mechanism of "branch
|
3103
|
+
upgrades." A branch upgrade is a migration containing SQL commands to effect
|
3104
|
+
the conversion of the head of the current branch's migration chain (i.e. the
|
3105
|
+
state of the database after the most recent migration in the current branch)
|
3106
|
+
into some state along another branch's migration chain. These commands are not
|
3107
|
+
applied when the upgrade script for this branch is run. Rather, they are saved
|
3108
|
+
in the database and run only if, and then prior to, an upgrade script for the
|
3109
|
+
targeted branch is run against the same database.
|
3110
|
+
|
3111
|
+
Unlike regular migrations, changes to the branch upgrade migration MAY be
|
3112
|
+
committed to the version control repository.
|
3113
|
+
|
3114
|
+
::::::::::::::::::::::::::::::::::: EXAMPLE :::::::::::::::::::::::::::::::::::
|
3115
|
+
:: ::
|
3116
|
+
|
3117
|
+
Product X is about to release version 2.0. Version 1.4 of Product X has
|
3118
|
+
been in customers' hands for 18 months and seven bug fixes involving database
|
3119
|
+
structure changes have been implemented in that time. Our example company has
|
3120
|
+
worked out the necessary SQL commands to convert the current head of the 1.4
|
3121
|
+
migration chain to the same resulting structure as the head of the 2.0
|
3122
|
+
migration chain. Those SQL commands, and the appropriate metadata about the
|
3123
|
+
target branch (version 2.0), the completed migration (the one named as the head
|
3124
|
+
of the 2.0 branch), and the head of the 1.4 branch are all put into the
|
3125
|
+
SCHEMA/structure/branch-upgrade.yaml file as detailed below. %program_name
|
3126
|
+
can then script the storage of these commands into the database for execution
|
3127
|
+
by a Product X version 2.0 upgrade script.
|
3128
|
+
|
3129
|
+
Once the branch-upgrade.yaml file is created and committed to version control
|
3130
|
+
in the version 1.4 branch, two upgrade scripts need to be generated: a
|
3131
|
+
version 1.4 upgrade script and a version 2.0 upgrade script, each from its
|
3132
|
+
respective branch in version control. For each version 1.4 installation, the
|
3133
|
+
1.4 script will first be run, bringing the installation up to the head of
|
3134
|
+
version 1.4 and installing the instructions for upgrading to a version 2.0
|
3135
|
+
database. Then the version 2.0 script will be run, which will execute the
|
3136
|
+
stored instructions for bringing the database from version 1.4 to version 2.0.
|
3137
|
+
|
3138
|
+
Had the branch upgrade not brought the version 1.4 database all the way up to
|
3139
|
+
the head of version 2.0 (i.e. if the YAML file indicates a completed migration
|
3140
|
+
prior to the version 2.0 head), the version 2.0 script would then apply any
|
3141
|
+
following migrations in order to bring the database up to the version 2.0 head.
|
3142
|
+
|
3143
|
+
:: ::
|
3144
|
+
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
3145
|
+
|
3146
|
+
In addition to the "sql" section, which has the same function as the "sql"
|
3147
|
+
section of a regular migration, the SCHEMA/structure/branch-upgrade.yaml file
|
3148
|
+
has three other required sections.
|
3149
|
+
|
3150
|
+
starting from
|
3151
|
+
-------------
|
3152
|
+
|
3153
|
+
As with regular migrations, the branch-upgrade.yaml specifies a "starting from"
|
3154
|
+
point, which should always be the head migration of the current branch (see
|
3155
|
+
SCHEMA/structure/head.yaml). If this section does not match with the latest
|
3156
|
+
migration in the migration chain, no branch upgrade information will be
|
3157
|
+
included in the resulting upgrade script and a warning will be issued during
|
3158
|
+
script generation. This precaution prevents out-of-date branch upgrade
|
3159
|
+
commands from being run.
|
3160
|
+
|
3161
|
+
resulting branch
|
3162
|
+
----------------
|
3163
|
+
|
3164
|
+
The "resulting branch" section must give the identifier of the branch
|
3165
|
+
containing the migration chain into which the included SQL commands will
|
3166
|
+
migrate the current chain head. This identifier can be obtained with the
|
3167
|
+
'%program_cmd branchid' command run against a working copy of the
|
3168
|
+
target branch.
|
3169
|
+
|
3170
|
+
completes migration to
|
3171
|
+
----------------------
|
3172
|
+
|
3173
|
+
Because the migration chain of the target branch is likely to be extended over
|
3174
|
+
time, it is necessary to pin down the intended result state of the branch
|
3175
|
+
upgrade to one particular migration in the target chain. The migration name
|
3176
|
+
listed in the "completes migration to" section should be the name of the
|
3177
|
+
migration (the file basename) which brings the target branch database to the
|
3178
|
+
same state as the head state of the current branch after applying the
|
3179
|
+
branch upgrade commands.
|
3180
|
+
END_SECTION
|
3181
|
+
end
|
3182
|
+
puts
|
3183
|
+
end
|
3184
|
+
|
3185
|
+
subcommand 'new', "Create a new migration file" do |argv|
|
3186
|
+
args, options = command_line(argv, {:edit=>true},
|
3187
|
+
:argument_desc=>"MIGRATION_SUMMARY",
|
3188
|
+
:help=> <<END_OF_HELP)
|
3189
|
+
This command generates a new migration file and ties it into the current
|
3190
|
+
migration chain. The name of the new file is generated from today's date and
|
3191
|
+
the given MIGRATION_SUMMARY. The resulting new file may be opened in an
|
3192
|
+
editor (see the --[no-]edit option).
|
3193
|
+
END_OF_HELP
|
3194
|
+
|
3195
|
+
argument_error_unless(args.length == 1,
|
3196
|
+
"'%prog %cmd' takes one argument.")
|
3197
|
+
migration_summary = args[0]
|
3198
|
+
argument_error_unless(
|
3199
|
+
migration_summary.chars.all? {|c| !ILLEGAL_FILENAME_CHARS.include?(c)},
|
3200
|
+
"Migration summary may not contain any of: " + ILLEGAL_FILENAME_CHARS
|
3201
|
+
)
|
3202
|
+
|
3203
|
+
tool = NewMigrationAdder.new(options.source_dir).extend(WarnToStderr)
|
3204
|
+
new_fpath = tool.add_migration(migration_summary)
|
3205
|
+
|
3206
|
+
edit(new_fpath) if options.edit
|
3207
|
+
end
|
3208
|
+
|
3209
|
+
subcommand 'upgrade', "Generate an upgrade script" do |argv|
|
3210
|
+
args, options = command_line(argv, {:production=>true, :outfile=>true},
|
3211
|
+
:help=> <<END_OF_HELP)
|
3212
|
+
Running this command will generate an update script from the source schema.
|
3213
|
+
Generation of a production script involves more checks on the status of the
|
3214
|
+
schema source files but produces a script that may be run on a development,
|
3215
|
+
production, or empty database. If the generated script is not specified for
|
3216
|
+
production it may only be run on a development or empty database; it will not
|
3217
|
+
run on production databases.
|
3218
|
+
END_OF_HELP
|
3219
|
+
|
3220
|
+
argument_error_unless(args.length == 0,
|
3221
|
+
"'%prog %cmd' does not take any arguments.")
|
3222
|
+
|
3223
|
+
sql_gen = SchemaUpdater.new(options.source_dir).extend(WarnToStderr)
|
3224
|
+
sql_gen.production = options.production
|
3225
|
+
|
3226
|
+
output_to(options.outfile) do |out_stream|
|
3227
|
+
out_stream.print(sql_gen.update_sql)
|
3228
|
+
end
|
3229
|
+
end
|
3230
|
+
|
3231
|
+
subcommand 'render', "Generate SQL script for an access object" do |argv|
|
3232
|
+
args, options = command_line(argv, {:outfile=>true},
|
3233
|
+
:argument_desc=>"ACCESS_OBJECT_FILE",
|
3234
|
+
:help=> <<END_OF_HELP)
|
3235
|
+
This command outputs the creation script for the access object defined in the
|
3236
|
+
file at path ACCESS_OBJECT_FILE, making any substitutions in the same way as
|
3237
|
+
the update script generator.
|
3238
|
+
END_OF_HELP
|
3239
|
+
|
3240
|
+
argument_error_unless(args.length == 1,
|
3241
|
+
"'%prog %cmd' takes one argument.")
|
3242
|
+
argument_error_unless(File.exist?(args[0]),
|
3243
|
+
"'%prog %cmd' must target an existing access object definition.")
|
3244
|
+
|
3245
|
+
sql_gen = SchemaUpdater.new(options.source_dir).extend(WarnToStderr)
|
3246
|
+
|
3247
|
+
artifact = sql_gen.access_artifacts[args[0]] || sql_gen.access_artifacts.at_path(args[0])
|
3248
|
+
output_to(options.outfile) do |out_stream|
|
3249
|
+
out_stream.print(artifact.creation_sql)
|
3250
|
+
end
|
3251
|
+
end
|
3252
|
+
|
3253
|
+
subcommand 'unbranch', "Fix a branched migration chain" do |argv|
|
3254
|
+
args, options = command_line(argv, {:dev_branch=>true},
|
3255
|
+
:help=> <<END_OF_HELP)
|
3256
|
+
Use this command to fix a branched migration chain. The need for this command
|
3257
|
+
usually arises when code is pulled from the repository into the working copy.
|
3258
|
+
|
3259
|
+
Because this program checks that migration files are unaltered when building
|
3260
|
+
a production upgrade script it is important to use this command only:
|
3261
|
+
|
3262
|
+
A) after updating in any branch, or
|
3263
|
+
|
3264
|
+
B) after merging into (including synching) a development branch
|
3265
|
+
|
3266
|
+
If used in case B, the resulting changes may make the migration chain in the
|
3267
|
+
current branch ineligible for generating production upgrade scripts. When the
|
3268
|
+
development branch is (cleanly) merged back to the production branch it will
|
3269
|
+
still be possible to generate a production upgrade script from the production
|
3270
|
+
branch. In case B the resulting script (generated in development mode) should
|
3271
|
+
be thoroughly tested.
|
3272
|
+
|
3273
|
+
Because of the potential danger to a production branch, this command checks
|
3274
|
+
the branch usage before executing. Inherent branch usage can be set through
|
3275
|
+
the 'productionpattern' command. If the target working copy has not been
|
3276
|
+
marked with a production-branch pattern, the branch usage is ambiguous and
|
3277
|
+
this command makes the fail-safe assumption that the branch is used for
|
3278
|
+
production. This assumption can be overriden with the --dev-branch option.
|
3279
|
+
Note that the --dev-branch option will NOT override the production-branch
|
3280
|
+
pattern if one exists.
|
3281
|
+
END_OF_HELP
|
3282
|
+
|
3283
|
+
argument_error_unless(args.length == 0,
|
3284
|
+
"'%prog %cmd' does not take any arguments.")
|
3285
|
+
|
3286
|
+
tool = SchemaManipulator.new(options.source_dir).extend(WarnToStderr)
|
3287
|
+
conflict = tool.get_conflict_info
|
3288
|
+
|
3289
|
+
unless conflict
|
3290
|
+
STDERR.puts("No conflict!")
|
3291
|
+
return
|
3292
|
+
end
|
3293
|
+
|
3294
|
+
if conflict.scope == :repository
|
3295
|
+
if conflict.branch_use == :production
|
3296
|
+
STDERR.puts(<<END_OF_MESSAGE)
|
3297
|
+
|
3298
|
+
The target working copy is on a production branch. Because fixing the branched
|
3299
|
+
migration chain would require modifying a committed migration, this operation
|
3300
|
+
would result in a migration chain incapable of producing a production upgrade
|
3301
|
+
script, leaving this branch unable to fulfill its purpose.
|
3302
|
+
|
3303
|
+
END_OF_MESSAGE
|
3304
|
+
raise(XMigra::Error, "Branch use conflict")
|
3305
|
+
end
|
3306
|
+
|
3307
|
+
dev_branch = (conflict.branch_use == :development) || options.dev_branch
|
3308
|
+
|
3309
|
+
unless dev_branch
|
3310
|
+
STDERR.puts(<<END_OF_MESSAGE)
|
3311
|
+
|
3312
|
+
The target working copy is neither marked for production branch recognition
|
3313
|
+
nor was the --dev-branch option given on the command line. Because fixing the
|
3314
|
+
branched migration chain would require modifying a committed migration, this
|
3315
|
+
operation would result in a migration chain incapable of producing a production
|
3316
|
+
upgrage which, because the usage of the working copy's branch is ambiguous,
|
3317
|
+
might leave this branch unable to fulfill its purpose.
|
3318
|
+
|
3319
|
+
END_OF_MESSAGE
|
3320
|
+
raise(XMigra::Error, "Potential branch use conflict")
|
3321
|
+
end
|
3322
|
+
|
3323
|
+
if conflict.branch_use == :undefined
|
3324
|
+
STDERR.puts(<<END_OF_MESSAGE)
|
3325
|
+
|
3326
|
+
The branch of the target working copy is not marked with a production branch
|
3327
|
+
recognition pattern. The --dev-branch option was given to override the
|
3328
|
+
ambiguity, but it is much safer to use the 'productionpattern' command to
|
3329
|
+
permanently mark the schema so that production branches can be automatically
|
3330
|
+
recognized.
|
3331
|
+
|
3332
|
+
END_OF_MESSAGE
|
3333
|
+
# Warning, not error
|
3334
|
+
end
|
3335
|
+
end
|
3336
|
+
|
3337
|
+
conflict.fix_conflict!
|
3338
|
+
end
|
3339
|
+
|
3340
|
+
subcommand 'productionpattern', "Set the recognition pattern for production branches" do |argv|
|
3341
|
+
args, options = command_line(argv, {},
|
3342
|
+
:argument_desc=>"PATTERN",
|
3343
|
+
:help=> <<END_OF_HELP)
|
3344
|
+
This command sets the production branch recognition pattern for the schema.
|
3345
|
+
The pattern given will determine whether this program treats the current
|
3346
|
+
working copy as a production or development branch. The PATTERN given
|
3347
|
+
is a Ruby Regexp that is used to evaluate the branch identifier of the working
|
3348
|
+
copy. Each supported version control system has its own type of branch
|
3349
|
+
identifier:
|
3350
|
+
|
3351
|
+
Subversion: The path within the repository, starting with a slash (e.g.
|
3352
|
+
"/trunk", "/branches/my-branch", "/foo/bar%20baz")
|
3353
|
+
|
3354
|
+
If PATTERN matches the branch identifier, the branch is considered to be a
|
3355
|
+
production branch. If PATTERN does not match, then the branch is a development
|
3356
|
+
branch. Some operations (e.g. 'unbranch') are prevented on production branches
|
3357
|
+
to avoid making the branch ineligible for generating production upgrade
|
3358
|
+
scripts.
|
3359
|
+
|
3360
|
+
In specifying PATTERN, it is not necessary to escape Ruby special characters
|
3361
|
+
(especially including the slash character), but special characters for the
|
3362
|
+
shell or command interpreter need their usual escaping. The matching algorithm
|
3363
|
+
used for PATTERN does not require the match to start at the beginning of the
|
3364
|
+
branch identifier; specify the anchor as part of PATTERN if desired.
|
3365
|
+
END_OF_HELP
|
3366
|
+
|
3367
|
+
argument_error_unless(args.length == 1,
|
3368
|
+
"'%prog %cmd' takes one argument.")
|
3369
|
+
Regexp.compile(args[0])
|
3370
|
+
|
3371
|
+
tool = SchemaManipulator.new(options.source_dir).extend(WarnToStderr)
|
3372
|
+
|
3373
|
+
tool.production_pattern = args[0]
|
3374
|
+
end
|
3375
|
+
|
3376
|
+
subcommand 'branchid', "Print the branch identifier string" do |argv|
|
3377
|
+
args, options = command_line(argv, {},
|
3378
|
+
:help=> <<END_OF_HELP)
|
3379
|
+
This command prints the branch identifier string to standard out (followed by
|
3380
|
+
a newline).
|
3381
|
+
END_OF_HELP
|
3382
|
+
|
3383
|
+
argument_error_unless(args.length == 0,
|
3384
|
+
"'%prog %cmd' does not take any arguments.")
|
3385
|
+
|
3386
|
+
tool = SchemaManipulator.new(options.source_dir).extend(WarnToStderr)
|
3387
|
+
|
3388
|
+
puts tool.branch_identifier
|
3389
|
+
end
|
3390
|
+
|
3391
|
+
subcommand 'history', "Show all SQL from migrations changing the target" do |argv|
|
3392
|
+
args, options = command_line(argv, {:outfile=>true, :target_type=>true, :search_type=>true},
|
3393
|
+
:argument_desc=>"TARGET [TARGET [...]]",
|
3394
|
+
:help=> <<END_OF_HELP)
|
3395
|
+
Use this command to get the SQL run by the upgrade script that modifies any of
|
3396
|
+
the specified TARGETs. By default this command uses a full item match against
|
3397
|
+
the contents of each item in each migration's "changes" key (i.e.
|
3398
|
+
--by=exact --match=changes). Migration SQL is printed in order of application
|
3399
|
+
to the database.
|
3400
|
+
END_OF_HELP
|
3401
|
+
|
3402
|
+
argument_error_unless(args.length >= 1,
|
3403
|
+
"'%prog %cmd' requires at least one argument.")
|
3404
|
+
|
3405
|
+
target_matches = case options.target_type
|
3406
|
+
when :substring
|
3407
|
+
proc {|subject| args.any? {|a| subject.include?(a)}}
|
3408
|
+
when :regexp
|
3409
|
+
patterns = args.map {|a| Regexp.compile(a)}
|
3410
|
+
proc {|subject| patterns.any? {|pat| pat.match(subject)}}
|
3411
|
+
else
|
3412
|
+
targets = Set.new(args)
|
3413
|
+
proc {|subject| targets.include?(subject)}
|
3414
|
+
end
|
3415
|
+
|
3416
|
+
criteria_met = case options.search_type
|
3417
|
+
when :sql
|
3418
|
+
proc {|migration| target_matches.call(migration.sql)}
|
3419
|
+
else
|
3420
|
+
proc {|migration| migration.changes.any? {|subject| target_matches.call(subject)}}
|
3421
|
+
end
|
3422
|
+
|
3423
|
+
tool = SchemaUpdater.new(options.source_dir).extend(WarnToStderr)
|
3424
|
+
|
3425
|
+
output_to(options.outfile) do |out_stream|
|
3426
|
+
tool.migrations.each do |migration|
|
3427
|
+
next unless criteria_met.call(migration)
|
3428
|
+
|
3429
|
+
out_stream << tool.sql_comment_block(File.basename(migration.file_path))
|
3430
|
+
out_stream << migration.sql
|
3431
|
+
out_stream << "\n" << tool.batch_separator if tool.respond_to? :batch_separator
|
3432
|
+
out_stream << "\n"
|
3433
|
+
end
|
3434
|
+
end
|
3435
|
+
end
|
3436
|
+
|
3437
|
+
subcommand 'permissions', "Generate a permission assignment script" do |argv|
|
3438
|
+
args, options = command_line(argv, {:outfile=>true},
|
3439
|
+
:help=> <<END_OF_HELP)
|
3440
|
+
This command generates and outputs a script that assigns permissions within
|
3441
|
+
a database instance. The permission information is read from the
|
3442
|
+
permissions.yaml file in the schema root directory (the same directory in which
|
3443
|
+
database.yaml resides) and has the format:
|
3444
|
+
|
3445
|
+
dbo.MyTable:
|
3446
|
+
Alice: SELECT
|
3447
|
+
Bob:
|
3448
|
+
- SELECT
|
3449
|
+
- INSERT
|
3450
|
+
|
3451
|
+
(More specifically: The top-level object is a mapping whose scalar keys are
|
3452
|
+
the names of the objects to be modified and whose values are mappings from
|
3453
|
+
security principals to either a single permission or a sequence of
|
3454
|
+
permissions.) The file is in YAML format; use quoted strings if necessary
|
3455
|
+
(e.g. for Microsoft SQL Server "square bracket escaping", enclose the name in
|
3456
|
+
single or double quotes within the permissions.yaml file to avoid
|
3457
|
+
interpretation of the square brackets as delimiting a sequence).
|
3458
|
+
|
3459
|
+
Before establishing the permissions listed in permissions.yaml, the generated
|
3460
|
+
script first removes any permissions previously granted through use of an
|
3461
|
+
XMigra permissions script. To accomplish this, the script establishes a table
|
3462
|
+
if it does not yet exist. The code for this precedes the code to remove
|
3463
|
+
previous permissions. Thus, the resulting script has the sequence:
|
3464
|
+
|
3465
|
+
- Establish permission tracking table (if not present)
|
3466
|
+
- Revoke all previously granted permissions (only those granted
|
3467
|
+
by a previous XMigra script)
|
3468
|
+
- Grant permissions indicated in permissions.yaml
|
3469
|
+
|
3470
|
+
To facilitate review of the script, the term "GRANT" is avoided except for
|
3471
|
+
the statements granting the permissions laid out in the source file.
|
3472
|
+
END_OF_HELP
|
3473
|
+
|
3474
|
+
argument_error_unless(args.length == 0,
|
3475
|
+
"'%prog %cmd' does not take any arguments.")
|
3476
|
+
|
3477
|
+
sql_gen = PermissionScriptWriter.new(options.source_dir).extend(WarnToStderr)
|
3478
|
+
|
3479
|
+
output_to(options.outfile) do |out_stream|
|
3480
|
+
out_stream.print(sql_gen.permissions_sql)
|
3481
|
+
end
|
3482
|
+
end
|
3483
|
+
end
|
3484
|
+
|
3485
|
+
def self.command_line_program
|
3486
|
+
XMigra::Program.run(
|
3487
|
+
ARGV,
|
3488
|
+
:error=>proc do |e|
|
3489
|
+
STDERR.puts("#{e} (#{e.class})") unless e.is_a?(XMigra::Program::QuietError)
|
3490
|
+
exit(2) if e.is_a?(OptionParser::ParseError)
|
3491
|
+
exit(2) if e.is_a?(XMigra::Program::ArgumentError)
|
3492
|
+
exit(1)
|
3493
|
+
end
|
3494
|
+
)
|
3495
|
+
end
|
3496
|
+
end
|
3497
|
+
|
3498
|
+
if $0 == __FILE__
|
3499
|
+
XMigra.command_line_program
|
3500
|
+
end
|