xmigra 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+
2
+ require 'xmigra/access_artifact'
3
+
4
+ module XMigra
5
+ class Function < AccessArtifact
6
+ OBJECT_TYPE = "FUNCTION"
7
+
8
+ # Construct with a hash (as if loaded from a function YAML file)
9
+ def initialize(func_info)
10
+ @name = func_info["name"].dup.freeze
11
+ @depends_on = func_info.fetch("referencing", []).dup.freeze
12
+ @definition = func_info["sql"].dup.freeze
13
+ end
14
+
15
+ attr_reader :name, :depends_on
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+
2
+ module XMigra
3
+ class Index
4
+ def initialize(index_info)
5
+ @name = index_info['name'].dup.freeze
6
+ @definition = index_info['sql'].dup.freeze
7
+ end
8
+
9
+ attr_reader :name
10
+
11
+ attr_accessor :file_path
12
+
13
+ def id
14
+ XMigra.secure_digest(@definition)
15
+ end
16
+
17
+ def definition_sql
18
+ @definition
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,38 @@
1
+
2
+ require 'xmigra/index'
3
+
4
+ module XMigra
5
+ class IndexCollection
6
+ def initialize(path, options={})
7
+ @items = Hash.new
8
+ db_specifics = options[:db_specifics]
9
+ Dir.glob(File.join(path, '*.yaml')).each do |fpath|
10
+ info = YAML.load_file(fpath)
11
+ info['name'] = File.basename(fpath, '.yaml')
12
+ index = Index.new(info)
13
+ index.extend(db_specifics) if db_specifics
14
+ index.file_path = File.expand_path(fpath)
15
+ @items[index.name] = index
16
+ end
17
+ end
18
+
19
+ def [](name)
20
+ @items[name]
21
+ end
22
+
23
+ def names
24
+ @items.keys
25
+ end
26
+
27
+ def each(&block); @items.each_value(&block); end
28
+ include Enumerable
29
+
30
+ def each_definition_sql
31
+ each {|i| yield i.definition_sql}
32
+ end
33
+
34
+ def empty?
35
+ @items.empty?
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,27 @@
1
+
2
+ module XMigra
3
+ class Migration
4
+ EMPTY_DB = 'empty database'
5
+ FOLLOWS = 'starting from'
6
+ CHANGES = 'changes'
7
+
8
+ def initialize(info)
9
+ @id = info['id'].dup.freeze
10
+ _follows = info[FOLLOWS]
11
+ @follows = (_follows.dup.freeze unless _follows == EMPTY_DB)
12
+ @sql = info["sql"].dup.freeze
13
+ @description = info["description"].dup.freeze
14
+ @changes = (info[CHANGES] || []).dup.freeze
15
+ @changes.each {|c| c.freeze}
16
+ end
17
+
18
+ attr_reader :id, :follows, :sql, :description, :changes
19
+ attr_accessor :file_path
20
+
21
+ class << self
22
+ def id_from_filename(fname)
23
+ XMigra.secure_digest(fname.upcase) # Base64 encoded digest
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,63 @@
1
+
2
+ require 'xmigra/migration'
3
+
4
+ module XMigra
5
+ class MigrationChain < Array
6
+ HEAD_FILE = 'head.yaml'
7
+ LATEST_CHANGE = 'latest change'
8
+ MIGRATION_FILE_PATTERN = /^\d{4}-\d\d-\d\d.*\.yaml$/i
9
+
10
+ def initialize(path, options={})
11
+ super()
12
+
13
+ db_specifics = options[:db_specifics]
14
+ vcs_specifics = options[:vcs_specifics]
15
+
16
+ head_info = YAML.load_file(File.join(path, HEAD_FILE))
17
+ file = head_info[LATEST_CHANGE]
18
+ prev_file = HEAD_FILE
19
+ files_loaded = []
20
+
21
+ until file.nil?
22
+ file = XMigra.yaml_path(file)
23
+ fpath = File.join(path, file)
24
+ break unless File.file?(fpath)
25
+ begin
26
+ mig_info = YAML.load_file(fpath)
27
+ rescue
28
+ raise XMigra::Error, "Error loading/parsing #{fpath}"
29
+ end
30
+ files_loaded << file
31
+ mig_info["id"] = Migration::id_from_filename(file)
32
+ migration = Migration.new(mig_info)
33
+ migration.file_path = File.expand_path(fpath)
34
+ migration.extend(db_specifics) if db_specifics
35
+ migration.extend(vcs_specifics) if vcs_specifics
36
+ unshift(migration)
37
+ prev_file = file
38
+ file = migration.follows
39
+ unless file.nil? || MIGRATION_FILE_PATTERN.match(XMigra.yaml_path(file))
40
+ raise XMigra::Error, "Invalid migration file \"#{file}\" referenced from \"#{prev_file}\""
41
+ end
42
+ end
43
+
44
+ @other_migrations = []
45
+ Dir.foreach(path) do |fname|
46
+ if MIGRATION_FILE_PATTERN.match(fname) && !files_loaded.include?(fname)
47
+ @other_migrations << fname.freeze
48
+ end
49
+ end
50
+ @other_migrations.freeze
51
+ end
52
+
53
+ # Test if the chain reaches back to the empty database
54
+ def complete?
55
+ length > 0 && self[0].follows.nil?
56
+ end
57
+
58
+ # Test if the chain encompasses all migration-like filenames in the path
59
+ def includes_all?
60
+ @other_migrations.empty?
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,68 @@
1
+
2
+ module XMigra
3
+ class MigrationConflict
4
+ def initialize(path, branch_point, heads)
5
+ @path = Pathname.new(path)
6
+ @branch_point = branch_point
7
+ @heads = heads
8
+ @branch_use = :undefined
9
+ @scope = :repository
10
+ @after_fix = nil
11
+ end
12
+
13
+ attr_accessor :branch_use, :scope, :after_fix
14
+
15
+ def resolvable?
16
+ head_0 = @heads[0]
17
+ @heads[1].each_pair do |k, v|
18
+ next unless head_0.has_key?(k)
19
+ next if k == MigrationChain::LATEST_CHANGE
20
+ return false unless head_0[k] == v
21
+ end
22
+
23
+ return true
24
+ end
25
+
26
+ def migration_tweak
27
+ unless defined? @migration_to_fix and defined? @fixed_migration_contents
28
+ # Walk the chain from @head[1][MigrationChain::LATEST_CHANGE] and find
29
+ # the first migration after @branch_point
30
+ branch_file = XMigra.yaml_path(@branch_point)
31
+ cur_mig = XMigra.yaml_path(@heads[1][MigrationChain::LATEST_CHANGE])
32
+ until cur_mig.nil?
33
+ mig_info = YAML.load_file(@path.join(cur_mig))
34
+ prev_mig = XMigra.yaml_path(mig_info[Migration::FOLLOWS])
35
+ break if prev_mig == branch_file
36
+ cur_mig = prev_mig
37
+ end
38
+
39
+ mig_info[Migration::FOLLOWS] = @heads[0][MigrationChain::LATEST_CHANGE]
40
+ @migration_to_fix = cur_mig
41
+ @fixed_migration_contents = mig_info
42
+ end
43
+
44
+ return @migration_to_fix, @fixed_migration_contents
45
+ end
46
+
47
+ def fix_conflict!
48
+ raise(VersionControlError, "Unresolvable conflict") unless resolvable?
49
+
50
+ file_to_fix, fixed_contents = migration_tweak
51
+
52
+ # Rewrite the head file
53
+ head_info = @heads[0].merge(@heads[1]) # This means @heads[1]'s LATEST_CHANGE wins
54
+ File.open(@path.join(MigrationChain::HEAD_FILE), 'w') do |f|
55
+ $xmigra_yamler.dump(head_info, f)
56
+ end
57
+
58
+ # Rewrite the first migration (on the current branch) after @branch_point
59
+ File.open(@path.join(file_to_fix), 'w') do |f|
60
+ $xmigra_yamler.dump(fixed_contents, f)
61
+ end
62
+
63
+ if @after_fix
64
+ @after_fix.call
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,4 @@
1
+
2
+ module XMigra
3
+
4
+ end
@@ -0,0 +1,74 @@
1
+
2
+ require 'xmigra/schema_manipulator'
3
+
4
+ module XMigra
5
+ class NewMigrationAdder < SchemaManipulator
6
+ OBSOLETE_VERINC_FILE = 'version-upgrade-obsolete.yaml'
7
+
8
+ def initialize(path)
9
+ super(path)
10
+ end
11
+
12
+ def add_migration(summary, options={})
13
+ struct_dir = @path.join(STRUCTURE_SUBDIR)
14
+ FileUtils.mkdir_p(struct_dir) unless struct_dir.exist?
15
+
16
+ # Load the head YAML from the structure subdir if it exists or create
17
+ # default empty migration chain
18
+ head_file = struct_dir.join(MigrationChain::HEAD_FILE)
19
+ head_info = if head_file.exist?
20
+ YAML.parse_file(head_file).transform
21
+ else
22
+ {}
23
+ end
24
+ Hash === head_info or raise XMigra::Error, "Invalid #{MigrationChain::HEAD_FILE} format"
25
+
26
+ new_fpath = struct_dir.join(
27
+ [Date.today.strftime("%Y-%m-%d"), summary].join(' ') + '.yaml'
28
+ )
29
+ raise(XMigra::Error, "Migration file\"#{new_fpath.basename}\" already exists") if new_fpath.exist?
30
+
31
+ new_data = {
32
+ Migration::FOLLOWS=>head_info.fetch(MigrationChain::LATEST_CHANGE, Migration::EMPTY_DB),
33
+ 'sql'=>options.fetch(:sql, "<<<<< INSERT SQL HERE >>>>>\n"),
34
+ 'description'=>options.fetch(:description, "<<<<< DESCRIPTION OF MIGRATION >>>>>").dup.extend(FoldedYamlStyle),
35
+ Migration::CHANGES=>options.fetch(:changes, ["<<<<< WHAT THIS MIGRATION CHANGES >>>>>"]),
36
+ }
37
+
38
+ # Write the head file first, in case a lock is required
39
+ old_head_info = head_info.dup
40
+ head_info[MigrationChain::LATEST_CHANGE] = new_fpath.basename('.yaml').to_s
41
+ File.open(head_file, "w") do |f|
42
+ $xmigra_yamler.dump(head_info, f)
43
+ end
44
+
45
+ begin
46
+ File.open(new_fpath, "w") do |f|
47
+ $xmigra_yamler.dump(new_data, f)
48
+ end
49
+ rescue
50
+ # Revert the head file to it's previous state
51
+ File.open(head_file, "w") do |f|
52
+ $xmigra_yamler.dump(old_head_info, f)
53
+ end
54
+
55
+ raise
56
+ end
57
+
58
+ # Obsolete any existing branch upgrade file
59
+ bufp = branch_upgrade_file
60
+ if bufp.exist?
61
+ warning("#{bufp.relative_path_from(@path)} is obsolete and will be renamed.") if respond_to? :warning
62
+
63
+ obufp = bufp.dirname.join(OBSOLETE_VERINC_FILE)
64
+ rm_method = respond_to?(:vcs_remove) ? method(:vcs_remove) : FileUtils.method(:rm)
65
+ mv_method = respond_to?(:vcs_move) ? method(:vcs_move) : FileUtils.method(:mv)
66
+
67
+ rm_method.call(obufp) if obufp.exist?
68
+ mv_method.call(bufp, obufp)
69
+ end
70
+
71
+ return new_fpath
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,67 @@
1
+
2
+ require 'xmigra/schema_manipulator'
3
+
4
+ module XMigra
5
+ class PermissionScriptWriter < SchemaManipulator
6
+ def initialize(path)
7
+ super(path)
8
+
9
+ @permissions = YAML.load_file(self.path + PERMISSIONS_FILE)
10
+ raise TypeError, "Expected Hash in #{PERMISSIONS_FILE}" unless Hash === @permissions
11
+ end
12
+
13
+ def in_ddl_transaction
14
+ yield
15
+ end
16
+
17
+ def ddl_block_separator; "\n"; end
18
+
19
+ def permissions_sql
20
+ intro_comment = @db_info.fetch('script comment', '') + "\n\n"
21
+
22
+ intro_comment + in_ddl_transaction do
23
+ [
24
+ # Check for blatantly incorrect application of script, e.g. running
25
+ # on master or template database.
26
+ check_execution_environment_sql,
27
+
28
+ # Create table for recording granted permissions if it doesn't exist
29
+ ensure_permissions_table_sql,
30
+
31
+ # Revoke permissions previously granted through an XMigra permissions
32
+ # script
33
+ revoke_previous_permissions_sql,
34
+
35
+ # Grant the permissions indicated in the source file
36
+ grant_specified_permissions_sql,
37
+
38
+ ].flatten.compact.join(ddl_block_separator)
39
+ end
40
+ end
41
+
42
+ def grant_specified_permissions_sql
43
+ granting_permissions_comment_sql +
44
+ enum_for(:each_specified_grant).map(&method(:grant_permissions_sql)).join("\n")
45
+ end
46
+
47
+ def each_specified_grant
48
+ @permissions.each_pair do |object, grants|
49
+ grants.each_pair do |principal, permissions|
50
+ permissions = [permissions] unless permissions.is_a? Enumerable
51
+ yield permissions, object, principal
52
+ end
53
+ end
54
+ end
55
+
56
+ def line_comment(contents)
57
+ "-- " + contents + " --\n"
58
+ end
59
+
60
+ def header(content, size)
61
+ dashes = size - content.length - 2
62
+ l_dashes = dashes / 2
63
+ r_dashes = dashes - l_dashes
64
+ ('-' * l_dashes) + ' ' + content + ' ' + ('-' * r_dashes)
65
+ end
66
+ end
67
+ end