xmigra 1.0.1

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