xmigra 1.0.1

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