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.
- checksums.yaml +4 -4
- data/lib/xmigra/access_artifact.rb +44 -0
- data/lib/xmigra/access_artifact_collection.rb +50 -0
- data/lib/xmigra/branch_upgrade.rb +62 -0
- data/lib/xmigra/db_support/mssql.rb +1070 -0
- data/lib/xmigra/function.rb +17 -0
- data/lib/xmigra/index.rb +21 -0
- data/lib/xmigra/index_collection.rb +38 -0
- data/lib/xmigra/migration.rb +27 -0
- data/lib/xmigra/migration_chain.rb +63 -0
- data/lib/xmigra/migration_conflict.rb +68 -0
- data/lib/xmigra/new_file.rb +4 -0
- data/lib/xmigra/new_migration_adder.rb +74 -0
- data/lib/xmigra/permission_script_writer.rb +67 -0
- data/lib/xmigra/program.rb +927 -0
- data/lib/xmigra/schema_manipulator.rb +39 -0
- data/lib/xmigra/schema_updater.rb +183 -0
- data/lib/xmigra/stored_procedure.rb +20 -0
- data/lib/xmigra/vcs_support/git.rb +275 -0
- data/lib/xmigra/vcs_support/svn.rb +213 -0
- data/lib/xmigra/version.rb +1 -1
- data/lib/xmigra/view.rb +17 -0
- data/lib/xmigra.rb +47 -3222
- data/test/runner.rb +53 -4
- metadata +22 -2
@@ -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
|
data/lib/xmigra/index.rb
ADDED
@@ -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,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
|