xmigra 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/xmigra.rb CHANGED
@@ -14,7 +14,6 @@ require "ostruct"
14
14
  require "pathname"
15
15
  require "rbconfig"
16
16
  require "rexml/document"
17
- require "tsort"
18
17
  require "yaml"
19
18
 
20
19
  require "xmigra/version"
@@ -217,90 +216,6 @@ module XMigra
217
216
 
218
217
  class SchemaError < Error; end
219
218
 
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
219
  class << self
305
220
  def access_artifact(info)
306
221
  case info["define"]
@@ -341,3159 +256,69 @@ module XMigra
341
256
  end
342
257
  end
343
258
 
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
259
+ module NoSpecifics; end
408
260
 
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
261
+ class VersionControlError < XMigra::Error; end
442
262
 
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
263
+ DatabaseSupportModules = []
264
+ VersionControlSupportModules = []
467
265
 
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?
266
+ module WarnToStderr
267
+ def warning(message)
268
+ STDERR.puts("Warning: " + message)
269
+ STDERR.puts
524
270
  end
525
271
  end
526
272
 
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
273
+ module FoldedYamlStyle
274
+ def to_yaml_style
275
+ :fold
590
276
  end
591
- end
592
-
593
- class BranchUpgrade
594
- TARGET_BRANCH = "resulting branch"
595
- MIGRATION_COMPLETED = "completes migration to"
596
277
 
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
278
+ if defined? Psych
279
+ def yaml_style
280
+ Psych::Nodes::Scalar::FOLDED
610
281
  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
282
  end
642
283
 
643
- private
644
-
645
- def warning(s)
646
- s.freeze
647
- @warnings << s
648
- end
649
284
  end
650
285
 
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"
286
+ def self.command_line_program
287
+ XMigra::Program.run(
288
+ ARGV,
289
+ :error=>proc do |e|
290
+ STDERR.puts("#{e} (#{e.class})") unless e.is_a?(XMigra::Program::QuietError)
291
+ exit(2) if e.is_a?(OptionParser::ParseError)
292
+ exit(2) if e.is_a?(XMigra::Program::ArgumentError)
293
+ exit(1)
940
294
  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
295
  )
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;
296
+ end
297
+ end
1212
298
 
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
299
+ require 'xmigra/vcs_support/svn'
300
+ require 'xmigra/vcs_support/git'
301
+
302
+ require 'xmigra/db_support/mssql'
303
+
304
+ require 'xmigra/access_artifact'
305
+ require 'xmigra/stored_procedure'
306
+ require 'xmigra/view'
307
+ require 'xmigra/function'
308
+
309
+ require 'xmigra/access_artifact_collection'
310
+ require 'xmigra/index'
311
+ require 'xmigra/index_collection'
312
+ require 'xmigra/migration'
313
+ require 'xmigra/migration_chain'
314
+ require 'xmigra/migration_conflict'
315
+ require 'xmigra/branch_upgrade'
316
+ require 'xmigra/schema_manipulator'
317
+ require 'xmigra/schema_updater'
318
+ require 'xmigra/new_migration_adder'
319
+ require 'xmigra/permission_script_writer'
320
+
321
+ require 'xmigra/program'
3497
322
 
3498
323
  if $0 == __FILE__
3499
324
  XMigra.command_line_program