schema-evolution-manager 0.9.24
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 +7 -0
- data/README.md +334 -0
- data/bin/sem-add +46 -0
- data/bin/sem-apply +41 -0
- data/bin/sem-baseline +34 -0
- data/bin/sem-config +1 -0
- data/bin/sem-dist +92 -0
- data/bin/sem-info +43 -0
- data/bin/sem-init +95 -0
- data/lib/schema-evolution-manager.rb +26 -0
- data/lib/schema-evolution-manager/apply_util.rb +60 -0
- data/lib/schema-evolution-manager/args.rb +159 -0
- data/lib/schema-evolution-manager/ask.rb +44 -0
- data/lib/schema-evolution-manager/baseline_util.rb +50 -0
- data/lib/schema-evolution-manager/db.rb +104 -0
- data/lib/schema-evolution-manager/install_template.rb +118 -0
- data/lib/schema-evolution-manager/library.rb +176 -0
- data/lib/schema-evolution-manager/migration_file.rb +93 -0
- data/lib/schema-evolution-manager/preconditions.rb +61 -0
- data/lib/schema-evolution-manager/rdoc_usage.rb +37 -0
- data/lib/schema-evolution-manager/script_error.rb +19 -0
- data/lib/schema-evolution-manager/scripts.rb +95 -0
- data/lib/schema-evolution-manager/sem_info.rb +72 -0
- data/lib/schema-evolution-manager/sem_version.rb +9 -0
- data/lib/schema-evolution-manager/template.rb +39 -0
- data/lib/schema-evolution-manager/version.rb +71 -0
- data/scripts/20130318-105434.sql +64 -0
- data/scripts/20130318-105456.sql +135 -0
- data/template/README.md +28 -0
- data/template/dev.rb +12 -0
- metadata +80 -0
@@ -0,0 +1,93 @@
|
|
1
|
+
module SchemaEvolutionManager
|
2
|
+
|
3
|
+
# Represents a single file to make a schema migration, typically
|
4
|
+
# stored in the scripts subdirectory.
|
5
|
+
class MigrationFile
|
6
|
+
|
7
|
+
class Attribute
|
8
|
+
|
9
|
+
attr_reader :name, :valid_values
|
10
|
+
|
11
|
+
def initialize(name, valid_values)
|
12
|
+
@name = Preconditions.check_not_blank(name, "name cannot be blank")
|
13
|
+
@valid_values = Preconditions.assert_class(valid_values, Array)
|
14
|
+
end
|
15
|
+
|
16
|
+
unless defined?(ATTRIBUTES)
|
17
|
+
ATTRIBUTES = [Attribute.new("transaction", ["single", "none"])]
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
class AttributeValue
|
23
|
+
|
24
|
+
attr_reader :attribute, :value
|
25
|
+
|
26
|
+
def initialize(attribute_name, value)
|
27
|
+
Preconditions.assert_class(attribute_name, String)
|
28
|
+
@value = Preconditions.assert_class(value, String)
|
29
|
+
|
30
|
+
@attribute = Attribute::ATTRIBUTES.find { |a| a.name == attribute_name }
|
31
|
+
Preconditions.check_not_null(@attribute,
|
32
|
+
"Attribute with name[%s] not found. Must be one of: %s" %
|
33
|
+
[attribute_name,
|
34
|
+
Attribute::ATTRIBUTES.map { |a| a.name }.join(" ")])
|
35
|
+
|
36
|
+
Preconditions.check_state(@attribute.valid_values.include?(@value),
|
37
|
+
"Attribute[%s] - Invalid value[%s]. Must be one of: %s" %
|
38
|
+
[@attribute.name, @value, @attribute.valid_values.join(" ")])
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
unless defined?(DEFAULTS)
|
44
|
+
DEFAULTS = [AttributeValue.new("transaction", "single")]
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_reader :path, :attribute_values
|
48
|
+
|
49
|
+
def initialize(path)
|
50
|
+
@path = path
|
51
|
+
Preconditions.check_state(File.exists?(@path), "File[#{@path}] does not exist")
|
52
|
+
@attribute_values = parse_attribute_values
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# Returns a list of AttributeValues from the file itself,
|
58
|
+
# including all defaults set by SEM. AttributeValues are defined
|
59
|
+
# in comments in the file.
|
60
|
+
def parse_attribute_values
|
61
|
+
values = []
|
62
|
+
each_property do |name, value|
|
63
|
+
values << AttributeValue.new(name, value)
|
64
|
+
end
|
65
|
+
|
66
|
+
DEFAULTS.each do |default|
|
67
|
+
if values.find { |v| v.attribute.name == default.attribute.name }.nil?
|
68
|
+
values << default
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
values
|
73
|
+
end
|
74
|
+
|
75
|
+
# Parse properties from the comments. Looks for this pattern:
|
76
|
+
#
|
77
|
+
# -- sem.attribute.name = value
|
78
|
+
#
|
79
|
+
# and yields each matching row with |name, value|
|
80
|
+
def each_property
|
81
|
+
IO.readlines(path).each do |l|
|
82
|
+
stripped = l.strip
|
83
|
+
if stripped.match(/^\-\-\s+sem\.attribute\./)
|
84
|
+
stripped.sub!(/^\-\-\s+sem\.attribute\./, '')
|
85
|
+
name, value = stripped.split(/\=/, 2).map(&:strip)
|
86
|
+
yield name, value
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module SchemaEvolutionManager
|
2
|
+
# Modeled after http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/base/Preconditions.html
|
3
|
+
class Preconditions
|
4
|
+
|
5
|
+
def Preconditions.check_argument(expression, error_message=nil)
|
6
|
+
if !expression
|
7
|
+
raise error_message || "check_argument failed"
|
8
|
+
end
|
9
|
+
nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def Preconditions.check_state(expression, error_message=nil)
|
13
|
+
if !expression
|
14
|
+
raise error_message || "check_state failed"
|
15
|
+
end
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def Preconditions.check_not_null(reference, error_message=nil)
|
20
|
+
if reference.nil?
|
21
|
+
raise error_message || "argument cannot be nil"
|
22
|
+
end
|
23
|
+
reference
|
24
|
+
end
|
25
|
+
|
26
|
+
def Preconditions.check_not_blank(reference, error_message=nil)
|
27
|
+
if reference.to_s.strip == ""
|
28
|
+
raise error_message || "argument cannot be blank"
|
29
|
+
end
|
30
|
+
reference
|
31
|
+
end
|
32
|
+
|
33
|
+
# Throws an error if opts is not empty. Useful when parsing
|
34
|
+
# arguments to a function
|
35
|
+
def Preconditions.assert_empty_opts(opts)
|
36
|
+
if !opts.empty?
|
37
|
+
raise "Invalid opts: #{opts.keys.inspect}\n#{opts.inspect}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Asserts that value is not nill and is_?(klass). Returns
|
42
|
+
# value. Common use is
|
43
|
+
#
|
44
|
+
# amount = Preconditions.assert_class(amount, BigDecimal)
|
45
|
+
def Preconditions.assert_class(value, klass)
|
46
|
+
Preconditions.check_not_null(value, "Value cannot be nil")
|
47
|
+
Preconditions.check_not_null(klass, "Klass cannot be nil")
|
48
|
+
Preconditions.check_state(value.is_a?(klass),
|
49
|
+
"Value is of type[#{value.class}] - class[#{klass}] is required. value[#{value.inspect.to_s}]")
|
50
|
+
value
|
51
|
+
end
|
52
|
+
|
53
|
+
def Preconditions.assert_class_or_nil(value, klass)
|
54
|
+
if !value.nil?
|
55
|
+
Preconditions.assert_class(value, klass)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module SchemaEvolutionManager
|
2
|
+
# Adapter for what we need to remove dependency on RDoc which was removed in ruby 1.9
|
3
|
+
class RdocUsage
|
4
|
+
|
5
|
+
def RdocUsage.message
|
6
|
+
path = Library.normalize_path(RdocUsage.program_name)
|
7
|
+
|
8
|
+
lines = []
|
9
|
+
|
10
|
+
IO.readlines(path).each do |line|
|
11
|
+
if !line.match(/^\#/)
|
12
|
+
break
|
13
|
+
end
|
14
|
+
if line.match(/^\#\!/)
|
15
|
+
next
|
16
|
+
end
|
17
|
+
lines << line.sub(/^\#\s?/, '').sub(/\n$/, '')
|
18
|
+
end
|
19
|
+
lines << ""
|
20
|
+
|
21
|
+
lines.join("\n")
|
22
|
+
end
|
23
|
+
|
24
|
+
def RdocUsage.printAndExit(exit_code=0)
|
25
|
+
puts RdocUsage.message
|
26
|
+
exit(exit_code)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def RdocUsage.program_name
|
31
|
+
$0
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module SchemaEvolutionManager
|
2
|
+
|
3
|
+
class ScriptError < Exception
|
4
|
+
|
5
|
+
attr_reader :filename
|
6
|
+
|
7
|
+
def initialize(db, filename)
|
8
|
+
@db = Preconditions.assert_class(db, Db)
|
9
|
+
@filename = Preconditions.assert_class(filename, String)
|
10
|
+
end
|
11
|
+
|
12
|
+
def dml
|
13
|
+
sql_command = "insert into %s.%s (filename) values ('%s')" % [Db.schema_name, Scripts::SCRIPTS, filename]
|
14
|
+
"psql --command \"%s\" %s" % [sql_command, @db.url]
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module SchemaEvolutionManager
|
2
|
+
|
3
|
+
class Scripts
|
4
|
+
|
5
|
+
if !defined?(SCRIPTS)
|
6
|
+
SCRIPTS = "scripts"
|
7
|
+
BOOTSTRAP_SCRIPTS = "bootstrap_scripts"
|
8
|
+
VALID_TABLE_NAMES = [BOOTSTRAP_SCRIPTS, SCRIPTS]
|
9
|
+
end
|
10
|
+
|
11
|
+
# @param db Instance of Db class
|
12
|
+
# @param table_name Name of the table used to record which scripts
|
13
|
+
# we have processed. Will be one of 'scripts' or 'bootstrap_scripts'
|
14
|
+
def initialize(db, table_name)
|
15
|
+
@db = Preconditions.assert_class(db, Db)
|
16
|
+
@table_name = Preconditions.assert_class(table_name, String)
|
17
|
+
Preconditions.check_state(VALID_TABLE_NAMES.include?(@table_name),
|
18
|
+
"Invalid table name[%s]. Must be one of: %s" % [@table_name, VALID_TABLE_NAMES.join(", ")])
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns a sorted list of the full file paths to any sql scripts in
|
22
|
+
# the specified directory
|
23
|
+
def Scripts.all(dir)
|
24
|
+
Preconditions.assert_class(dir, String)
|
25
|
+
|
26
|
+
if File.directory?(dir)
|
27
|
+
Dir.glob("#{dir}/*.sql").sort
|
28
|
+
else
|
29
|
+
[]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# For each sql script that needs to be applied to this database,
|
34
|
+
# yields a pair of |filename, fullpath| in proper order
|
35
|
+
#
|
36
|
+
# db = Db.new(host, user, name)
|
37
|
+
# scripts = Scripts.new(db)
|
38
|
+
# scripts.each_pending do |filename, path|
|
39
|
+
# puts filename
|
40
|
+
# end
|
41
|
+
def each_pending(dir)
|
42
|
+
files = {}
|
43
|
+
Scripts.all(dir).each do |path|
|
44
|
+
name = File.basename(path)
|
45
|
+
files[name] = path
|
46
|
+
end
|
47
|
+
|
48
|
+
scripts_previously_run(files.keys).each do |filename|
|
49
|
+
files.delete(filename)
|
50
|
+
end
|
51
|
+
|
52
|
+
files.keys.sort.each do |filename|
|
53
|
+
## We have to recheck if this script is still pending. Some
|
54
|
+
## upgrade scripts may modify the scripts table themselves. This
|
55
|
+
## is actually useful in cases like when we migrated gilt from
|
56
|
+
## util_schema => schema_evolution_manager schema
|
57
|
+
if !has_run?(filename)
|
58
|
+
yield filename, files[filename]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# True if this script has already been applied to the db. False
|
64
|
+
# otherwise.
|
65
|
+
def has_run?(filename)
|
66
|
+
if @db.schema_schema_evolution_manager_exists?
|
67
|
+
query = "select count(*) from %s.%s where filename = '%s'" % [Db.schema_name, @table_name, filename]
|
68
|
+
@db.psql_command(query).to_i > 0
|
69
|
+
else
|
70
|
+
false
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Inserts a record to indiciate that we have loaded the specified file.
|
75
|
+
def record_as_run!(filename)
|
76
|
+
Preconditions.check_state(filename.match(/^\d\d\d\d\d\d+\-\d\d\d\d\d\d\.sql$/),
|
77
|
+
"Invalid filename[#{filename}]. Must be like: 20120503-173242.sql")
|
78
|
+
command = "insert into %s.%s (filename) select '%s' where not exists (select 1 from %s.%s where filename = '%s')" % [Db.schema_name, @table_name, filename, Db.schema_name, @table_name, filename]
|
79
|
+
@db.psql_command(command)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
# Fetch the list of scripts that have already been applied to this
|
84
|
+
# database.
|
85
|
+
def scripts_previously_run(scripts)
|
86
|
+
if scripts.empty? || !@db.schema_schema_evolution_manager_exists?
|
87
|
+
[]
|
88
|
+
else
|
89
|
+
sql = "select filename from %s.%s where filename in (%s)" % [Db.schema_name, @table_name, "'" + scripts.join("', '") + "'"]
|
90
|
+
@db.psql_command(sql).strip.split
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module SchemaEvolutionManager
|
2
|
+
|
3
|
+
module SemInfo
|
4
|
+
|
5
|
+
def SemInfo.version(args=nil)
|
6
|
+
SchemaEvolutionManager::SemVersion::VERSION.dup
|
7
|
+
end
|
8
|
+
|
9
|
+
def SemInfo.tag(args)
|
10
|
+
valid = ['exists', 'latest', 'next']
|
11
|
+
|
12
|
+
subcommand = args.shift.to_s.strip
|
13
|
+
|
14
|
+
if subcommand == "exists"
|
15
|
+
tag = args.shift.to_s.strip
|
16
|
+
if tag.empty?
|
17
|
+
puts "ERROR: Missing tag."
|
18
|
+
exit(3)
|
19
|
+
elsif ::SchemaEvolutionManager::Library.tag_exists?(tag)
|
20
|
+
puts "true"
|
21
|
+
else
|
22
|
+
puts "false"
|
23
|
+
end
|
24
|
+
|
25
|
+
elsif subcommand == "latest"
|
26
|
+
if latest = ::SchemaEvolutionManager::SemInfo::Tag.latest
|
27
|
+
latest.to_version_string
|
28
|
+
else
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
elsif subcommand == "next"
|
33
|
+
::SchemaEvolutionManager::SemInfo::Tag.next(args).to_version_string
|
34
|
+
|
35
|
+
elsif subcommand.empty?
|
36
|
+
puts "ERROR: Missing tag subcommand. Must be one of: %s" % valid.join(", ")
|
37
|
+
exit(3)
|
38
|
+
|
39
|
+
else
|
40
|
+
puts "ERROR: Invalid tag subcommand[%s]. Must be one of: %s" % [subcommand, valid.join(", ")]
|
41
|
+
exit(4)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module Tag
|
46
|
+
|
47
|
+
def Tag.latest
|
48
|
+
Library.latest_tag || Version.new("0.0.0")
|
49
|
+
end
|
50
|
+
|
51
|
+
# @param component: One of major|minor|micro. Defaults to micro. Currently passed in as an array
|
52
|
+
def Tag.next(args=nil)
|
53
|
+
component = (args || []).first
|
54
|
+
valid = ['micro', 'minor', 'major']
|
55
|
+
|
56
|
+
if component.to_s.empty?
|
57
|
+
component = "micro"
|
58
|
+
end
|
59
|
+
|
60
|
+
if valid.include?(component)
|
61
|
+
latest.send("next_%s" % component)
|
62
|
+
else
|
63
|
+
puts "ERROR: Invalid component[%s]. Must be one of: %s" % [component, valid.join(", ")]
|
64
|
+
exit(4)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module SchemaEvolutionManager
|
2
|
+
|
3
|
+
# Simple template parsing using regular expressions to substitute values.
|
4
|
+
# See unit test for example usage at test/specs/lib/template_spec.rb
|
5
|
+
class Template
|
6
|
+
|
7
|
+
class Substitution
|
8
|
+
|
9
|
+
attr_reader :pattern, :value
|
10
|
+
|
11
|
+
def initialize(pattern, value)
|
12
|
+
@pattern = Preconditions.check_not_blank(pattern)
|
13
|
+
@value = Preconditions.check_not_blank(value)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@subs = []
|
20
|
+
end
|
21
|
+
|
22
|
+
# add('first', 'Mike')
|
23
|
+
# Will replace all instances of '%%first%%' with value when you call parse
|
24
|
+
def add(pattern, value)
|
25
|
+
@subs << Substitution.new(pattern, value)
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse(contents)
|
29
|
+
Preconditions.check_not_blank(contents)
|
30
|
+
string = contents.dup
|
31
|
+
@subs.each do |sub|
|
32
|
+
string = string.gsub(/%%#{sub.pattern}%%/, sub.value)
|
33
|
+
end
|
34
|
+
string
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module SchemaEvolutionManager
|
2
|
+
|
3
|
+
class Version
|
4
|
+
|
5
|
+
VERSION_FILE = File.join(Library.base_dir, "VERSION") unless defined?(VERSION_FILE)
|
6
|
+
|
7
|
+
attr_reader :major, :minor, :micro
|
8
|
+
|
9
|
+
def initialize(version_string)
|
10
|
+
Preconditions.check_not_blank(version_string, "version_string cannot be blank")
|
11
|
+
Library.assert_valid_tag(version_string)
|
12
|
+
pieces = version_string.split(".", 3)
|
13
|
+
@major = pieces[0].to_i
|
14
|
+
@minor = pieces[1].to_i
|
15
|
+
@micro = pieces[2].to_i
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_version_string
|
19
|
+
"%s.%s.%s" % [major, minor, micro]
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the next major version
|
23
|
+
def next_major
|
24
|
+
Version.new("%s.%s.%s" % [major+1, 0, 0])
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the next minor version
|
28
|
+
def next_minor
|
29
|
+
Version.new("%s.%s.%s" % [major, minor+1, 0])
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the next micro version
|
33
|
+
def next_micro
|
34
|
+
Version.new("%s.%s.%s" % [major, minor, micro+1])
|
35
|
+
end
|
36
|
+
|
37
|
+
def <=>(other)
|
38
|
+
Preconditions.assert_class(other, Version)
|
39
|
+
value = major <=> other.major
|
40
|
+
if value == 0
|
41
|
+
value = minor <=> other.minor
|
42
|
+
if value == 0
|
43
|
+
value = micro <=> other.micro
|
44
|
+
end
|
45
|
+
end
|
46
|
+
value
|
47
|
+
end
|
48
|
+
|
49
|
+
# Reads the current version (from the VERSION FILE), returning an
|
50
|
+
# instance of the Version class
|
51
|
+
def Version.read
|
52
|
+
Preconditions.check_state(File.exists?(VERSION_FILE), "Version file at path[%s] not found" % VERSION_FILE)
|
53
|
+
version = IO.read(VERSION_FILE).strip
|
54
|
+
Version.new(version)
|
55
|
+
end
|
56
|
+
|
57
|
+
def Version.write(version)
|
58
|
+
Preconditions.assert_class(version, Version)
|
59
|
+
File.open(VERSION_FILE, "w") do |out|
|
60
|
+
out << "%s\n" % version.to_version_string
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def Version.is_valid?(version_string)
|
65
|
+
Preconditions.check_not_blank(version_string, "version_string cannot be blank")
|
66
|
+
version_string.match(/^\d+\.\d+\.\d+$/)
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|