schema-evolution-manager 0.9.24
Sign up to get free protection for your applications and to get access to all the features.
- 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,44 @@
|
|
1
|
+
module SchemaEvolutionManager
|
2
|
+
|
3
|
+
# Simple library to ask user for input, with easy mocakability for
|
4
|
+
# testing
|
5
|
+
class Ask
|
6
|
+
|
7
|
+
TRUE_STRINGS = ['y', 'yes'] unless defined?(TRUE_STRINGS)
|
8
|
+
|
9
|
+
# Asks the user a question. Expects a string back.
|
10
|
+
def Ask.for_string(message, opts={})
|
11
|
+
default = opts.delete(:default)
|
12
|
+
Preconditions.assert_empty_opts(opts)
|
13
|
+
|
14
|
+
final_message = message.dup
|
15
|
+
if default
|
16
|
+
final_message << " [%s] " % default
|
17
|
+
end
|
18
|
+
|
19
|
+
value = nil
|
20
|
+
while value.to_s == ""
|
21
|
+
print final_message
|
22
|
+
value = get_input.strip
|
23
|
+
if value.to_s == "" && default
|
24
|
+
value = default.to_s.strip
|
25
|
+
end
|
26
|
+
end
|
27
|
+
value
|
28
|
+
end
|
29
|
+
|
30
|
+
# Asks the user a question. Returns a boolean. Boolean is defined as
|
31
|
+
# matching the strings 'y' or 'yes', case insensitive
|
32
|
+
def Ask.for_boolean(message)
|
33
|
+
value = Ask.for_string("%s (y/n) " % message)
|
34
|
+
TRUE_STRINGS.include?(value.downcase)
|
35
|
+
end
|
36
|
+
|
37
|
+
# here to help with tests
|
38
|
+
def Ask.get_input
|
39
|
+
STDIN.gets
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module SchemaEvolutionManager
|
2
|
+
|
3
|
+
class BaselineUtil
|
4
|
+
|
5
|
+
def initialize(db, opts={})
|
6
|
+
@dry_run = opts.delete(:dry_run)
|
7
|
+
if @dry_run.nil?
|
8
|
+
@dry_run = true
|
9
|
+
end
|
10
|
+
|
11
|
+
@db = Preconditions.assert_class(db, Db)
|
12
|
+
@scripts = Scripts.new(@db, Scripts::SCRIPTS)
|
13
|
+
end
|
14
|
+
|
15
|
+
def dry_run?
|
16
|
+
@dry_run
|
17
|
+
end
|
18
|
+
|
19
|
+
# Applies scripts in order, returning number of scripts applied
|
20
|
+
def apply!(dir)
|
21
|
+
Preconditions.check_state(File.directory?(dir),
|
22
|
+
"Dir[%s] does not exist" % dir)
|
23
|
+
|
24
|
+
count = 0
|
25
|
+
@scripts.each_pending(dir) do |filename, path|
|
26
|
+
count += 1
|
27
|
+
if @dry_run
|
28
|
+
puts "[DRY RUN] Baselining #{filename}"
|
29
|
+
apply_dry_run(filename, path)
|
30
|
+
else
|
31
|
+
puts "Baselining #{filename}"
|
32
|
+
apply_real(filename, path)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
count
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def apply_dry_run(filename, path)
|
40
|
+
puts path
|
41
|
+
puts ""
|
42
|
+
end
|
43
|
+
|
44
|
+
def apply_real(filename, path)
|
45
|
+
@scripts.record_as_run!(filename)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module SchemaEvolutionManager
|
2
|
+
|
3
|
+
class Db
|
4
|
+
|
5
|
+
attr_reader :url
|
6
|
+
|
7
|
+
def initialize(url)
|
8
|
+
@url = Preconditions.check_not_blank(url, "url cannot be blank")
|
9
|
+
end
|
10
|
+
|
11
|
+
# Installs schema_evolution_manager. Automatically upgrades schema_evolution_manager.
|
12
|
+
def bootstrap!
|
13
|
+
scripts = Scripts.new(self, Scripts::BOOTSTRAP_SCRIPTS)
|
14
|
+
dir = File.join(Library.base_dir, "scripts")
|
15
|
+
scripts.each_pending(dir) do |filename, path|
|
16
|
+
psql_file(path)
|
17
|
+
scripts.record_as_run!(filename)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# executes a simple sql command.
|
22
|
+
def psql_command(sql_command)
|
23
|
+
Preconditions.assert_class(sql_command, String)
|
24
|
+
command = "psql --no-align --tuples-only --command \"%s\" %s" % [sql_command, @url]
|
25
|
+
Library.system_or_error(command)
|
26
|
+
end
|
27
|
+
|
28
|
+
def Db.attribute_values(path)
|
29
|
+
Preconditions.assert_class(path, String)
|
30
|
+
|
31
|
+
options = []
|
32
|
+
|
33
|
+
['quiet', 'no-align', 'tuples-only'].each do |v|
|
34
|
+
options << "--#{v}"
|
35
|
+
end
|
36
|
+
|
37
|
+
SchemaEvolutionManager::MigrationFile.new(path).attribute_values.map do |value|
|
38
|
+
if value.attribute.name == "transaction"
|
39
|
+
if value.value == "single"
|
40
|
+
options << "--single-transaction"
|
41
|
+
elsif value.value == "none"
|
42
|
+
# No-op
|
43
|
+
else
|
44
|
+
raise "File[%s] - attribute[%s] unsupported value[%s]" % [path, value.attribute.name, value.value]
|
45
|
+
end
|
46
|
+
else
|
47
|
+
raise "File[%s] - unsupported attribute named[%s]" % [path, value.attribute.name]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
options
|
52
|
+
end
|
53
|
+
|
54
|
+
# executes sql commands from a file in a single transaction
|
55
|
+
def psql_file(path)
|
56
|
+
Preconditions.assert_class(path, String)
|
57
|
+
Preconditions.check_state(File.exists?(path), "File[%s] not found" % path)
|
58
|
+
|
59
|
+
options = Db.attribute_values(path).join(" ")
|
60
|
+
|
61
|
+
Library.with_temp_file(:prefix => File.basename(path)) do |tmp|
|
62
|
+
File.open(tmp, "w") do |out|
|
63
|
+
out << "\\set ON_ERROR_STOP true\n\n"
|
64
|
+
out << IO.read(path)
|
65
|
+
end
|
66
|
+
|
67
|
+
command = "psql --file \"%s\" #{options} %s" % [tmp, @url]
|
68
|
+
Library.system_or_error(command)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# True if the specific schema exists; false otherwise
|
73
|
+
def schema_schema_evolution_manager_exists?
|
74
|
+
sql = "select count(*) from pg_namespace where nspname='%s'" % Db.schema_name
|
75
|
+
psql_command(sql).to_i > 0
|
76
|
+
end
|
77
|
+
|
78
|
+
# Parses command line arguments returning an instance of
|
79
|
+
# Db. Exists if invalid config.
|
80
|
+
def Db.parse_command_line_config(arg_string)
|
81
|
+
Preconditions.assert_class(arg_string, String)
|
82
|
+
args = Args.new(arg_string, :optional => ['url', 'host', 'user', 'name', 'port'])
|
83
|
+
Db.from_args(args)
|
84
|
+
end
|
85
|
+
|
86
|
+
def Db.from_args(args)
|
87
|
+
Preconditions.assert_class(args, Args)
|
88
|
+
if args.url
|
89
|
+
Db.new(args.url)
|
90
|
+
else
|
91
|
+
base = "%s:%s/%s" % [args.host || "localhost", args.port || 5432, args.name]
|
92
|
+
url = args.user ? "%s@%s" % [args.user, base] : base
|
93
|
+
Db.new("postgres://" + url)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Returns the name of the schema_evolution_manager schema
|
98
|
+
def Db.schema_name
|
99
|
+
"schema_evolution_manager"
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module SchemaEvolutionManager
|
2
|
+
|
3
|
+
class InstallTemplate
|
4
|
+
|
5
|
+
def initialize(opts={})
|
6
|
+
@lib_dir = Preconditions.check_not_blank(opts.delete(:lib_dir), "lib_dir is required")
|
7
|
+
@bin_dir = Preconditions.check_not_blank(opts.delete(:bin_dir), "bin_dir is required")
|
8
|
+
Preconditions.assert_empty_opts(opts)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Generates the actual contents of the install file
|
12
|
+
def generate
|
13
|
+
template = Template.new
|
14
|
+
template.add('timestamp', Time.now.to_s)
|
15
|
+
template.add('lib_dir', @lib_dir)
|
16
|
+
template.add('bin_dir', @bin_dir)
|
17
|
+
template.parse(TEMPLATE)
|
18
|
+
end
|
19
|
+
|
20
|
+
def write_to_file(path)
|
21
|
+
puts "Writing %s" % path
|
22
|
+
File.open(path, "w") do |out|
|
23
|
+
out << generate
|
24
|
+
end
|
25
|
+
Library.system_or_error("chmod +x %s" % path)
|
26
|
+
end
|
27
|
+
|
28
|
+
if !defined?(TEMPLATE)
|
29
|
+
TEMPLATE = <<-"EOS"
|
30
|
+
#!/usr/bin/env ruby
|
31
|
+
#
|
32
|
+
# Generated on %%timestamp%%
|
33
|
+
#
|
34
|
+
|
35
|
+
load File.join(File.dirname(__FILE__), 'lib/schema-evolution-manager.rb')
|
36
|
+
SchemaEvolutionManager::Library.set_verbose(true)
|
37
|
+
|
38
|
+
lib_dir = '%%lib_dir%%'
|
39
|
+
bin_dir = '%%bin_dir%%'
|
40
|
+
version = SchemaEvolutionManager::Version.read
|
41
|
+
|
42
|
+
version_name = "schema-evolution-manager-%s" % version.to_version_string
|
43
|
+
version_dir = File.join(lib_dir, version_name)
|
44
|
+
version_bin_dir = File.join(version_dir, 'bin')
|
45
|
+
|
46
|
+
SchemaEvolutionManager::Library.ensure_dir!(version_dir)
|
47
|
+
SchemaEvolutionManager::Library.ensure_dir!(bin_dir)
|
48
|
+
|
49
|
+
Dir.chdir(lib_dir) do
|
50
|
+
if File.exists?("schema-evolution-manager")
|
51
|
+
if File.symlink?("schema-evolution-manager")
|
52
|
+
SchemaEvolutionManager::Library.system_or_error("rm schema-evolution-manager")
|
53
|
+
SchemaEvolutionManager::Library.system_or_error("ln -s %s %s" % [version_name, 'schema-evolution-manager'])
|
54
|
+
else
|
55
|
+
puts "*** WARNING: File[%s] already exists. Not creating symlink" % File.join(lib_dir, "schema-evolution-manager")
|
56
|
+
end
|
57
|
+
else
|
58
|
+
SchemaEvolutionManager::Library.system_or_error("ln -s %s %s" % [version_name, 'schema-evolution-manager'])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
['CONVENTIONS.md', 'LICENSE', 'README.md', 'VERSION'].each do |filename|
|
63
|
+
SchemaEvolutionManager::Library.system_or_error("cp %s %s" % [filename, version_dir])
|
64
|
+
end
|
65
|
+
|
66
|
+
['bin', 'lib', 'lib/schema-evolution-manager', 'template', 'scripts'].each do |dir|
|
67
|
+
this_dir = File.join(version_dir, dir)
|
68
|
+
SchemaEvolutionManager::Library.ensure_dir!(this_dir)
|
69
|
+
Dir.foreach(dir) do |filename|
|
70
|
+
path = File.join(dir, filename)
|
71
|
+
if File.file?(path)
|
72
|
+
SchemaEvolutionManager::Library.system_or_error("cp %s %s" % [path, this_dir])
|
73
|
+
if dir == "bin" && filename != "sem-config"
|
74
|
+
SchemaEvolutionManager::Library.system_or_error("chmod +x %s/%s" % [this_dir, filename])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
aliased_bin_dir = File.join(lib_dir, "schema-evolution-manager", "bin")
|
81
|
+
Dir.chdir(bin_dir) do
|
82
|
+
Dir.foreach(aliased_bin_dir) do |filename|
|
83
|
+
path = File.join(aliased_bin_dir, filename)
|
84
|
+
if File.file?(path)
|
85
|
+
SchemaEvolutionManager::Library.system_or_error("rm -f %s && ln -s %s" % [filename, path])
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Overrwrite bin/sem-config with proper location of lib dir
|
91
|
+
init_file = File.join(version_dir, "bin/sem-config")
|
92
|
+
SchemaEvolutionManager::Preconditions.check_state(File.exists?(init_file), "Init file[%s] not found" % init_file)
|
93
|
+
File.open(init_file, "w") do |out|
|
94
|
+
out << "load File.join('%s')\n" % File.join(version_dir, 'lib/schema-evolution-manager.rb')
|
95
|
+
end
|
96
|
+
|
97
|
+
puts ""
|
98
|
+
puts "schema-evolution-manager %s installed in %s" % [version.to_version_string, version_dir]
|
99
|
+
puts " - lib dir: %s" % lib_dir
|
100
|
+
puts " - bin dir: %s" % bin_dir
|
101
|
+
puts ""
|
102
|
+
|
103
|
+
found = `which sem-add`.strip
|
104
|
+
if found == ""
|
105
|
+
puts "Recommend adding the bin directory to your path"
|
106
|
+
puts " export PATH=%s:$PATH" % bin_dir
|
107
|
+
puts ""
|
108
|
+
end
|
109
|
+
|
110
|
+
puts "installation complete"
|
111
|
+
puts ""
|
112
|
+
|
113
|
+
EOS
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
module SchemaEvolutionManager
|
2
|
+
class Library
|
3
|
+
|
4
|
+
unless defined?(TMPFILE_DIR)
|
5
|
+
TMPFILE_DIR = "/tmp"
|
6
|
+
TMPFILE_PREFIX = "schema-evolution-manager-#{Process.pid}.tmp"
|
7
|
+
end
|
8
|
+
@@tmpfile_count = 0
|
9
|
+
@@verbose = false
|
10
|
+
|
11
|
+
# Creates the dir if it does not already exist
|
12
|
+
def Library.ensure_dir!(dir)
|
13
|
+
Preconditions.assert_class(dir, String)
|
14
|
+
|
15
|
+
if !File.directory?(dir)
|
16
|
+
Library.system_or_error("mkdir -p #{dir}")
|
17
|
+
end
|
18
|
+
Library.assert_dir_exists(dir)
|
19
|
+
end
|
20
|
+
|
21
|
+
def Library.assert_dir_exists(dir)
|
22
|
+
Preconditions.assert_class(dir, String)
|
23
|
+
|
24
|
+
if !File.directory?(dir)
|
25
|
+
raise "Dir[#{dir}] does not exist"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def Library.format_time(timestamp=Time.now)
|
30
|
+
timestamp.strftime('%Y-%m-%d %H:%M:%S %Z')
|
31
|
+
end
|
32
|
+
|
33
|
+
def Library.git_assert_tag_exists(tag)
|
34
|
+
command = "git tag -l"
|
35
|
+
results = Library.system_or_error(command)
|
36
|
+
if results.nil?
|
37
|
+
raise "No git tags found"
|
38
|
+
end
|
39
|
+
|
40
|
+
if !Library.tag_exists?(tag)
|
41
|
+
raise "Tag[#{tag}] not found. Check #{command}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def Library.assert_valid_tag(tag)
|
46
|
+
Preconditions.check_state(Version.is_valid?(tag), "Invalid tag[%s]. Format must be x.x.x (e.g. 1.1.2)" % tag)
|
47
|
+
end
|
48
|
+
|
49
|
+
def Library.git_has_remote?
|
50
|
+
system("git config --get remote.origin.url")
|
51
|
+
end
|
52
|
+
|
53
|
+
# Fetches the latest tag from the git repo. Returns nil if there are
|
54
|
+
# no tags, otherwise returns an instance of Version. Only searches for
|
55
|
+
# tags matching x.x.x (e.g. 1.0.2)
|
56
|
+
def Library.latest_tag
|
57
|
+
`git tag -l`.strip.split.select { |tag| Version.is_valid?(tag) }.map { |tag| Version.new(tag) }.sort.last
|
58
|
+
end
|
59
|
+
|
60
|
+
def Library.tag_exists?(tag)
|
61
|
+
`git tag -l`.strip.include?(tag)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Ex: Library.git_create_tag("0.0.1")
|
65
|
+
def Library.git_create_tag(tag)
|
66
|
+
Library.assert_valid_tag(tag)
|
67
|
+
has_remote = Library.git_has_remote?
|
68
|
+
if has_remote
|
69
|
+
Library.system_or_error("git fetch --tags origin")
|
70
|
+
end
|
71
|
+
Library.system_or_error("git tag -a -m #{tag} #{tag}")
|
72
|
+
if has_remote
|
73
|
+
Library.system_or_error("git push --tags origin")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Generates a temp file name, yield the full path to the
|
78
|
+
# file. Cleans up automatically on exit.
|
79
|
+
def Library.with_temp_file(opts={})
|
80
|
+
prefix = opts.delete(:prefix)
|
81
|
+
Preconditions.assert_empty_opts(opts)
|
82
|
+
|
83
|
+
if prefix.to_s == ""
|
84
|
+
prefix = TMPFILE_PREFIX
|
85
|
+
end
|
86
|
+
path = File.join(TMPFILE_DIR, "%s.%s" % [prefix, @@tmpfile_count])
|
87
|
+
@@tmpfile_count += 1
|
88
|
+
yield path
|
89
|
+
ensure
|
90
|
+
Library.delete_file_if_exists(path)
|
91
|
+
end
|
92
|
+
|
93
|
+
def Library.delete_file_if_exists(path)
|
94
|
+
if File.exists?(path)
|
95
|
+
FileUtils.rm_r(path)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Writes the string to a temp file, yielding the path. Cleans up on
|
100
|
+
# exit.
|
101
|
+
def Library.write_to_temp_file(string)
|
102
|
+
Library.with_temp_file do |path|
|
103
|
+
File.open(path, "w") do |out|
|
104
|
+
out << string
|
105
|
+
end
|
106
|
+
yield path
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns the relative path to the base directory (root of this git
|
111
|
+
# repo)
|
112
|
+
def Library.base_dir
|
113
|
+
@@base_dir
|
114
|
+
end
|
115
|
+
|
116
|
+
def Library.set_base_dir(value)
|
117
|
+
Preconditions.check_state(File.directory?(value), "Dir[%s] not found" % value)
|
118
|
+
@@base_dir = Library.normalize_path(value)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Runs the specified command, raising an error if there is a problem
|
122
|
+
# (based on status code of the process executed). Otherwise returns
|
123
|
+
# all the output from the script invoked.
|
124
|
+
def Library.system_or_error(command)
|
125
|
+
if Library.is_verbose?
|
126
|
+
puts command
|
127
|
+
end
|
128
|
+
|
129
|
+
begin
|
130
|
+
result = `#{command}`.strip
|
131
|
+
status = $?
|
132
|
+
if status.to_i > 0
|
133
|
+
raise "Non zero exit code[%s] running command[%s]" % [status, command]
|
134
|
+
end
|
135
|
+
rescue Exception => e
|
136
|
+
raise "Error running command[%s]: %s" % [command, e.to_s]
|
137
|
+
end
|
138
|
+
result
|
139
|
+
end
|
140
|
+
|
141
|
+
def Library.normalize_path(path)
|
142
|
+
Pathname.new(path).cleanpath.to_s
|
143
|
+
end
|
144
|
+
|
145
|
+
def Library.is_verbose?
|
146
|
+
@@verbose
|
147
|
+
end
|
148
|
+
|
149
|
+
def Library.set_verbose(value)
|
150
|
+
@@verbose = value ? true : false
|
151
|
+
end
|
152
|
+
|
153
|
+
# Returns a formatted string of git commits made since the specified tag.
|
154
|
+
def Library.git_changes(opts={})
|
155
|
+
tag = opts.delete(:tag)
|
156
|
+
number_changes = opts.delete(:number_changes) || 10
|
157
|
+
Preconditions.check_state(number_changes > 0)
|
158
|
+
Preconditions.assert_empty_opts(opts)
|
159
|
+
|
160
|
+
git_log_command = "git log --pretty=format:\"%h %ad | %s%d [%an]\" --date=short -#{number_changes}"
|
161
|
+
git_log = Library.system_or_error(git_log_command)
|
162
|
+
out = ""
|
163
|
+
out << "Created: %s\n" % Library.format_time
|
164
|
+
if tag
|
165
|
+
out << "Git Tag: %s\n" % tag
|
166
|
+
end
|
167
|
+
out << "\n"
|
168
|
+
out << "%s:\n" % git_log_command
|
169
|
+
out << " " << git_log.split("\n").join("\n ") << "\n"
|
170
|
+
out
|
171
|
+
end
|
172
|
+
|
173
|
+
@@base_dir = Library.normalize_path(File.join(File.dirname(__FILE__), ".."))
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|