You need to sign in or sign up before continuing.
schema-evolution-manager 0.9.74 → 0.9.75
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/README.md +9 -3
- data/bin/sem-add +2 -3
- data/bin/sem-apply +8 -10
- data/bin/sem-baseline +5 -5
- data/bin/sem-info +11 -5
- data/bin/sem-init +3 -18
- data/lib/schema-evolution-manager/apply_util.rb +7 -33
- data/lib/schema-evolution-manager/args.rb +8 -7
- data/lib/schema-evolution-manager/ask.rb +8 -2
- data/lib/schema-evolution-manager/baseline_util.rb +6 -39
- data/lib/schema-evolution-manager/db.rb +113 -21
- data/lib/schema-evolution-manager/library.rb +13 -10
- data/lib/schema-evolution-manager/migration_file.rb +6 -0
- data/lib/schema-evolution-manager/pending_scripts_util.rb +30 -0
- data/lib/schema-evolution-manager/rdoc_usage.rb +29 -0
- data/lib/schema-evolution-manager/script_error.rb +19 -4
- data/lib/schema-evolution-manager/scripts.rb +49 -2
- data/lib/schema-evolution-manager/sem_info.rb +11 -31
- data/lib/schema-evolution-manager/sem_version.rb +1 -1
- data/lib/schema-evolution-manager/version.rb +10 -6
- data/lib/schema-evolution-manager.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ab56a47b14bfc595e3cdad6ba15edef70ea02b50dbdb1c4b43a72e832937ca20
|
|
4
|
+
data.tar.gz: d035c43de9a7e0de1d12ff1f95fcc9acde7fdf0916b2827f5f1bbb604c317260
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6f1eb21b2c631d38a38806274f2f7e87d6ad15e74f28b742842a3a746a3e0c2ecf4e4e517fd2494c61fbc15c9d2b3f7917fd13d201778d1b129147e909f27200
|
|
7
|
+
data.tar.gz: 3c24490ba5ee1e285c8e28dbb36cb79f0a441f9a9d9efa02ff3cca8f3027ed2d64b0356033826e4e0b99db0ef73a00c907afa2b4e812f560817d6a4945b0bc01
|
data/README.md
CHANGED
|
@@ -149,7 +149,7 @@ There are three ways to install schema evolution manager:
|
|
|
149
149
|
|
|
150
150
|
git clone git@github.com:mbryzek/schema-evolution-manager.git
|
|
151
151
|
cd schema-evolution-manager
|
|
152
|
-
git checkout 0.9.
|
|
152
|
+
git checkout 0.9.75
|
|
153
153
|
ruby ./configure.rb
|
|
154
154
|
sudo ./install.rb
|
|
155
155
|
|
|
@@ -244,7 +244,13 @@ deploy process.
|
|
|
244
244
|
|
|
245
245
|
sem-apply --url postgresql://postgres@localhost/sample --dry_run
|
|
246
246
|
|
|
247
|
-
|
|
247
|
+
A dry run writes nothing to the database: not your scripts, and not sem's own
|
|
248
|
+
schema either. On a database sem has not yet installed itself in, it lists the
|
|
249
|
+
bootstrap scripts that would create that schema (see data model section below):
|
|
250
|
+
|
|
251
|
+
[DRY RUN] Bootstrapping 20130318-105434.sql
|
|
252
|
+
|
|
253
|
+
You should also see:
|
|
248
254
|
|
|
249
255
|
[DRY RUN] Applying 20130318-214407.sql
|
|
250
256
|
|
|
@@ -259,7 +265,7 @@ There are two recommended ways in which to pass user passwords to psql:
|
|
|
259
265
|
file](http://www.postgresql.org/docs/9.4/static/libpq-pgpass.html)
|
|
260
266
|
with the appropriate credentials</li>
|
|
261
267
|
|
|
262
|
-
2. Specify a [--password] flag when running sem-apply. You will
|
|
268
|
+
2. Specify a [--password] flag when running sem-apply or sem-baseline. You will
|
|
263
269
|
then be prompted to enter your password once. sem will create a
|
|
264
270
|
temporary file to store your password, using that file during the
|
|
265
271
|
duration of the command and ensuring the file is deleted after sem
|
data/bin/sem-add
CHANGED
|
@@ -13,12 +13,11 @@ load File.join(File.dirname(__FILE__), 'sem-config')
|
|
|
13
13
|
|
|
14
14
|
file = ARGV.shift
|
|
15
15
|
if file.to_s.strip == ""
|
|
16
|
-
|
|
17
|
-
SchemaEvolutionManager::RdocUsage.printAndExit(1)
|
|
16
|
+
SchemaEvolutionManager::RdocUsage.error!("Missing file path", SchemaEvolutionManager::RdocUsage::MISSING_INPUT, :usage => true)
|
|
18
17
|
end
|
|
19
18
|
|
|
20
19
|
SchemaEvolutionManager::Preconditions.check_state(File.exist?(file), "File[#{file}] could not be found")
|
|
21
|
-
SchemaEvolutionManager::Preconditions.check_state(file.match(/\.sql/i), "File[#{file}] must end with .sql")
|
|
20
|
+
SchemaEvolutionManager::Preconditions.check_state(file.match?(/\.sql\z/i), "File[#{file}] must end with .sql")
|
|
22
21
|
|
|
23
22
|
scripts_dir = SchemaEvolutionManager::Config.load.scripts_dir
|
|
24
23
|
SchemaEvolutionManager::Library.ensure_dir!(scripts_dir)
|
data/bin/sem-apply
CHANGED
|
@@ -15,20 +15,14 @@ require 'tempfile'
|
|
|
15
15
|
|
|
16
16
|
load File.join(File.dirname(__FILE__), 'sem-config')
|
|
17
17
|
|
|
18
|
-
args = SchemaEvolutionManager::Args.from_stdin(:optional => %w(
|
|
19
|
-
|
|
20
|
-
password = if args.password
|
|
21
|
-
SchemaEvolutionManager::Ask.for_password("Please enter your password: ")
|
|
22
|
-
else
|
|
23
|
-
nil
|
|
24
|
-
end
|
|
18
|
+
args = SchemaEvolutionManager::Args.from_stdin(:optional => SchemaEvolutionManager::Db::CONNECTION_FLAGS + %w(dry_run password))
|
|
19
|
+
db = SchemaEvolutionManager::Db.from_command_line(args)
|
|
25
20
|
|
|
26
21
|
config = SchemaEvolutionManager::Config.load
|
|
27
|
-
db = SchemaEvolutionManager::Db.from_args(args, :password => password)
|
|
28
22
|
util = SchemaEvolutionManager::ApplyUtil.new(db, :dry_run => args.dry_run || false)
|
|
29
23
|
|
|
30
24
|
begin
|
|
31
|
-
db.bootstrap!
|
|
25
|
+
db.bootstrap!(:dry_run => util.dry_run?)
|
|
32
26
|
|
|
33
27
|
puts "Upgrading schema for #{db.sanitized_url}"
|
|
34
28
|
count = util.apply!(config.scripts_dir)
|
|
@@ -59,7 +53,11 @@ rescue SchemaEvolutionManager::ScriptError => e
|
|
|
59
53
|
end
|
|
60
54
|
|
|
61
55
|
puts ""
|
|
62
|
-
|
|
56
|
+
if e.applied?
|
|
57
|
+
puts "This script was applied, but recording it as applied failed. Record it before the next apply, which would otherwise run it again:"
|
|
58
|
+
else
|
|
59
|
+
puts "If this script has previously been applied to this database, you can record it as having been applied by:"
|
|
60
|
+
end
|
|
63
61
|
puts " " + e.dml
|
|
64
62
|
puts ""
|
|
65
63
|
exit(1)
|
data/bin/sem-baseline
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
-
# == Makes sure that sem is
|
|
2
|
+
# == Makes sure that sem is bootstrapped, and then adds all existing scripts to the sem-database without applying
|
|
3
3
|
# Useful for migration to sem
|
|
4
4
|
#
|
|
5
5
|
# == Usage
|
|
@@ -9,19 +9,19 @@
|
|
|
9
9
|
#
|
|
10
10
|
# == Examples
|
|
11
11
|
# sem-baseline --url postgresql://postgres@localhost/sample
|
|
12
|
-
# sem-
|
|
12
|
+
# sem-baseline --host localhost --user web --name test
|
|
13
13
|
#
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
load File.join(File.dirname(__FILE__), 'sem-config')
|
|
17
17
|
|
|
18
|
-
args = SchemaEvolutionManager::Args.from_stdin(:optional => %w(
|
|
18
|
+
args = SchemaEvolutionManager::Args.from_stdin(:optional => SchemaEvolutionManager::Db::CONNECTION_FLAGS + %w(dry_run password))
|
|
19
|
+
db = SchemaEvolutionManager::Db.from_command_line(args)
|
|
19
20
|
|
|
20
21
|
config = SchemaEvolutionManager::Config.load
|
|
21
|
-
db = SchemaEvolutionManager::Db.from_args(args)
|
|
22
|
-
db.bootstrap!
|
|
23
22
|
|
|
24
23
|
dry_run = args.dry_run.nil? ? false : args.dry_run
|
|
24
|
+
db.bootstrap!(:dry_run => dry_run)
|
|
25
25
|
|
|
26
26
|
util = SchemaEvolutionManager::BaselineUtil.new(db, :dry_run => dry_run)
|
|
27
27
|
|
data/bin/sem-info
CHANGED
|
@@ -29,6 +29,13 @@
|
|
|
29
29
|
# libraries. Initially designed to help with wrapper scripts that
|
|
30
30
|
# needed information (like the next minor tag)
|
|
31
31
|
#
|
|
32
|
+
# Exit status
|
|
33
|
+
#
|
|
34
|
+
# Errors go to stderr only, so stdout holds nothing but the answer.
|
|
35
|
+
# 1: the command failed (e.g. the database could not be read)
|
|
36
|
+
# 3: missing input (a command, subcommand, tag or connection)
|
|
37
|
+
# 4: invalid input (an unrecognized value or a malformed --url)
|
|
38
|
+
#
|
|
32
39
|
|
|
33
40
|
load File.join(File.dirname(__FILE__), 'sem-config')
|
|
34
41
|
|
|
@@ -43,10 +50,9 @@ elsif command == "db"
|
|
|
43
50
|
elsif command == "version"
|
|
44
51
|
puts SchemaEvolutionManager::SemInfo.version(ARGV)
|
|
45
52
|
|
|
53
|
+
elsif command == ""
|
|
54
|
+
SchemaEvolutionManager::RdocUsage.error!("Missing command", SchemaEvolutionManager::RdocUsage::MISSING_INPUT, :usage => true)
|
|
55
|
+
|
|
46
56
|
else
|
|
47
|
-
|
|
48
|
-
puts "ERROR: Unrecognized command[%s]" % command
|
|
49
|
-
puts ""
|
|
50
|
-
end
|
|
51
|
-
SchemaEvolutionManager::RdocUsage.printAndExit(1)
|
|
57
|
+
SchemaEvolutionManager::RdocUsage.error!("Unrecognized command[%s]" % command, SchemaEvolutionManager::RdocUsage::INVALID_INPUT, :usage => true)
|
|
52
58
|
end
|
data/bin/sem-init
CHANGED
|
@@ -21,15 +21,9 @@
|
|
|
21
21
|
load File.join(File.dirname(__FILE__), 'sem-config')
|
|
22
22
|
SchemaEvolutionManager::Library.set_verbose(true)
|
|
23
23
|
|
|
24
|
-
args = SchemaEvolutionManager::Args.from_stdin(:required => %w(dir), :optional => %w(
|
|
24
|
+
args = SchemaEvolutionManager::Args.from_stdin(:required => %w(dir), :optional => SchemaEvolutionManager::Db::CONNECTION_FLAGS + %w(scripts_dir))
|
|
25
25
|
SchemaEvolutionManager::Preconditions.check_state(File.directory?(args.dir), "Dir[%s] does not exist" % args.dir)
|
|
26
|
-
|
|
27
|
-
if args.url.nil? && args.name.nil?
|
|
28
|
-
puts "Must specify either url or name"
|
|
29
|
-
exit(1)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
db = SchemaEvolutionManager::Db.from_args(args)
|
|
26
|
+
db = SchemaEvolutionManager::Db.from_command_line(args)
|
|
33
27
|
|
|
34
28
|
def copy_file(source, target, substitutions)
|
|
35
29
|
template = SchemaEvolutionManager::Template.new
|
|
@@ -72,14 +66,6 @@ Dir.chdir(args.dir) do
|
|
|
72
66
|
SchemaEvolutionManager::Library.system_or_error(["git", "commit", "-m", "Add schema-evolution-manager wrapper scripts", "--"] + wrappers)
|
|
73
67
|
end
|
|
74
68
|
|
|
75
|
-
readme = "README.md"
|
|
76
|
-
if !File.exist?(readme)
|
|
77
|
-
puts "Creating #{readme}"
|
|
78
|
-
copy_file("schema-evolution-manager/template/README.md", "README.md", subs)
|
|
79
|
-
SchemaEvolutionManager::Library.system_or_error(["git", "add", "--", readme])
|
|
80
|
-
SchemaEvolutionManager::Library.system_or_error(["git", "commit", "-m", "Adding README.md", "--", readme])
|
|
81
|
-
end
|
|
82
|
-
|
|
83
69
|
if args.scripts_dir
|
|
84
70
|
config_file = SchemaEvolutionManager::Config::FILENAME
|
|
85
71
|
if File.exist?(config_file)
|
|
@@ -104,8 +90,7 @@ Dir.chdir(args.dir) do
|
|
|
104
90
|
end
|
|
105
91
|
|
|
106
92
|
# Create the first git tag if necessary
|
|
107
|
-
|
|
108
|
-
if tags.nil? || tags == ""
|
|
93
|
+
if SchemaEvolutionManager::Library.git_tags.empty?
|
|
109
94
|
puts "Creating initial git tag (0.0.1)"
|
|
110
95
|
SchemaEvolutionManager::Library.git_create_tag("0.0.1")
|
|
111
96
|
end
|
|
@@ -1,41 +1,15 @@
|
|
|
1
1
|
module SchemaEvolutionManager
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Runs each pending script and records it as run.
|
|
4
|
+
class ApplyUtil < PendingScriptsUtil
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
6
|
+
private
|
|
7
|
+
def label
|
|
8
|
+
"Applying"
|
|
17
9
|
end
|
|
18
10
|
|
|
19
|
-
|
|
20
|
-
|
|
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] Applying #{filename}"
|
|
29
|
-
puts path
|
|
30
|
-
puts ""
|
|
31
|
-
else
|
|
32
|
-
print "Applying #{filename}"
|
|
33
|
-
@db.psql_file(filename, path)
|
|
34
|
-
@scripts.record_as_run!(filename)
|
|
35
|
-
puts " Done"
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
count
|
|
11
|
+
def execute?
|
|
12
|
+
true
|
|
39
13
|
end
|
|
40
14
|
|
|
41
15
|
end
|
|
@@ -61,6 +61,10 @@ module SchemaEvolutionManager
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
found_arguments = parse_string_arguments(args)
|
|
64
|
+
not_accepted = found_arguments.keys - required - optional
|
|
65
|
+
Preconditions.check_state(not_accepted.empty?,
|
|
66
|
+
"Flag[%s] is not accepted by this command. Accepted: %s" %
|
|
67
|
+
[not_accepted.map { |f| "--#{f}" }.join(" "), (required + optional).map { |f| "--#{f}" }.join(" ")])
|
|
64
68
|
missing = required.select { |field| blank?(found_arguments[field]) }
|
|
65
69
|
|
|
66
70
|
@artifact_name = found_arguments.delete(:artifact_name)
|
|
@@ -76,7 +80,7 @@ module SchemaEvolutionManager
|
|
|
76
80
|
@set = found_arguments.delete(:set) || []
|
|
77
81
|
|
|
78
82
|
@dry_run = found_arguments.delete(:dry_run)
|
|
79
|
-
@
|
|
83
|
+
@non_interactive = found_arguments.delete(:non_interactive)
|
|
80
84
|
@password = found_arguments.delete(:password)
|
|
81
85
|
@help = found_arguments.delete(:help)
|
|
82
86
|
@verbose = found_arguments.delete(:verbose)
|
|
@@ -120,12 +124,8 @@ module SchemaEvolutionManager
|
|
|
120
124
|
title = fields.size == 1 ? "Missing parameter" : "Missing parameters"
|
|
121
125
|
sorted = fields.sort_by { |f| f.to_s }
|
|
122
126
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
puts "**************************************************"
|
|
126
|
-
puts help_parameters("Required parameters", required)
|
|
127
|
-
puts help_parameters("Optional parameters", optional)
|
|
128
|
-
exit(1)
|
|
127
|
+
details = [help_parameters("Required parameters", required), help_parameters("Optional parameters", optional)]
|
|
128
|
+
RdocUsage.error!("#{title}: #{sorted.join(", ")}", RdocUsage::MISSING_INPUT, :details => details)
|
|
129
129
|
end
|
|
130
130
|
|
|
131
131
|
def help_parameters(title, parameters)
|
|
@@ -155,6 +155,7 @@ module SchemaEvolutionManager
|
|
|
155
155
|
index += 1
|
|
156
156
|
|
|
157
157
|
if FLAGS_WITH_ARGUMENTS.has_key?(flag)
|
|
158
|
+
Preconditions.check_state(index < values.length, "Flag[--%s] needs a value" % flag)
|
|
158
159
|
if ARRAY_FLAGS.include?(flag)
|
|
159
160
|
found[flag] ||= []
|
|
160
161
|
found[flag] << values[index]
|
|
@@ -13,7 +13,7 @@ module SchemaEvolutionManager
|
|
|
13
13
|
# to the screen. If false, we do NOT echo.
|
|
14
14
|
def Ask.for_string(message, opts={})
|
|
15
15
|
default = opts.delete(:default)
|
|
16
|
-
echo = opts
|
|
16
|
+
echo = opts.has_key?(:echo) ? opts.delete(:echo) : true
|
|
17
17
|
Preconditions.assert_empty_opts(opts)
|
|
18
18
|
|
|
19
19
|
final_message = message.dup
|
|
@@ -24,9 +24,15 @@ module SchemaEvolutionManager
|
|
|
24
24
|
value = nil
|
|
25
25
|
while value.to_s == ""
|
|
26
26
|
print final_message
|
|
27
|
-
|
|
27
|
+
input = get_input(echo)
|
|
28
|
+
value = input.to_s.strip
|
|
28
29
|
if value.to_s == "" && default
|
|
29
30
|
value = default.to_s.strip
|
|
31
|
+
elsif input.nil?
|
|
32
|
+
# Standard input is closed (a pipe, /dev/null, a CI job): asking
|
|
33
|
+
# again reads nothing again, forever.
|
|
34
|
+
puts ""
|
|
35
|
+
raise "No input for[%s]: standard input is closed" % message.strip
|
|
30
36
|
end
|
|
31
37
|
end
|
|
32
38
|
value
|
|
@@ -1,48 +1,15 @@
|
|
|
1
1
|
module SchemaEvolutionManager
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
3
|
+
# Records each pending script as run without running it.
|
|
4
|
+
class BaselineUtil < PendingScriptsUtil
|
|
37
5
|
|
|
38
6
|
private
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
puts ""
|
|
7
|
+
def label
|
|
8
|
+
"Baselining"
|
|
42
9
|
end
|
|
43
10
|
|
|
44
|
-
def
|
|
45
|
-
|
|
11
|
+
def execute?
|
|
12
|
+
false
|
|
46
13
|
end
|
|
47
14
|
|
|
48
15
|
end
|
|
@@ -2,6 +2,17 @@ module SchemaEvolutionManager
|
|
|
2
2
|
|
|
3
3
|
class Db
|
|
4
4
|
|
|
5
|
+
# Every flag that names a database connection. A command that opens a
|
|
6
|
+
# Db accepts these plus its own flags (dry_run, password, scripts_dir)
|
|
7
|
+
CONNECTION_FLAGS = %w(url host port name user set) unless defined?(CONNECTION_FLAGS)
|
|
8
|
+
|
|
9
|
+
# The connection flags do not name a database: neither --url nor
|
|
10
|
+
# --name, or a --url that is not a connection string
|
|
11
|
+
class InvalidConnection < RuntimeError; end
|
|
12
|
+
|
|
13
|
+
# Neither --url nor --name was given
|
|
14
|
+
class MissingConnection < InvalidConnection; end
|
|
15
|
+
|
|
5
16
|
# psql_args: the psql executable and its global options, as an argv
|
|
6
17
|
attr_reader :url, :psql_args
|
|
7
18
|
|
|
@@ -36,22 +47,43 @@ module SchemaEvolutionManager
|
|
|
36
47
|
end
|
|
37
48
|
|
|
38
49
|
# Installs schema_evolution_manager. Automatically upgrades schema_evolution_manager.
|
|
39
|
-
|
|
50
|
+
#
|
|
51
|
+
# @param dry_run: When true, prints each bootstrap script that would run
|
|
52
|
+
# and writes nothing: the schema is neither created nor upgraded. The
|
|
53
|
+
# pending-script reads answer for an absent schema, so a dry run of the
|
|
54
|
+
# scripts that follow needs no bootstrap. Returns the number of
|
|
55
|
+
# bootstrap scripts run (or, in a dry run, that would run).
|
|
56
|
+
def bootstrap!(opts={})
|
|
57
|
+
dry_run = opts.delete(:dry_run) || false
|
|
58
|
+
Preconditions.assert_empty_opts(opts)
|
|
59
|
+
|
|
40
60
|
scripts = Scripts.new(self, Scripts::BOOTSTRAP_SCRIPTS)
|
|
41
61
|
dir = File.join(Library.base_dir, "scripts")
|
|
62
|
+
count = 0
|
|
42
63
|
scripts.each_pending(dir) do |filename, path|
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
count += 1
|
|
65
|
+
if dry_run
|
|
66
|
+
puts "[DRY RUN] Bootstrapping #{filename}"
|
|
67
|
+
else
|
|
68
|
+
psql_file(filename, path, scripts)
|
|
69
|
+
scripts.record_as_run!(filename)
|
|
70
|
+
end
|
|
45
71
|
end
|
|
72
|
+
count
|
|
46
73
|
end
|
|
47
74
|
|
|
48
75
|
# executes a simple sql command.
|
|
49
76
|
def psql_command(sql_command)
|
|
50
|
-
|
|
51
|
-
command = @psql_args + PSQL_ISOLATION_OPTIONS + ["--no-align", "--tuples-only", "--command", sql_command]
|
|
77
|
+
command = psql_command_args(sql_command)
|
|
52
78
|
Library.system_or_error(command + [@url], :log => command + [sanitized_url])
|
|
53
79
|
end
|
|
54
80
|
|
|
81
|
+
# The psql argv psql_command runs for sql_command, without the url.
|
|
82
|
+
def psql_command_args(sql_command)
|
|
83
|
+
Preconditions.assert_class(sql_command, String)
|
|
84
|
+
@psql_args + PSQL_ISOLATION_OPTIONS + ["--no-align", "--tuples-only", "--command", sql_command]
|
|
85
|
+
end
|
|
86
|
+
|
|
55
87
|
def Db.attribute_values(path)
|
|
56
88
|
Preconditions.assert_class(path, String)
|
|
57
89
|
|
|
@@ -78,26 +110,55 @@ module SchemaEvolutionManager
|
|
|
78
110
|
options
|
|
79
111
|
end
|
|
80
112
|
|
|
81
|
-
#
|
|
82
|
-
# psql reads the original file, so the line numbers and path
|
|
83
|
-
# any psql error match the file in git.
|
|
84
|
-
|
|
113
|
+
# Applies the sql file at path and records filename as run in
|
|
114
|
+
# scripts. psql reads the original file, so the line numbers and path
|
|
115
|
+
# in any psql error match the file in git.
|
|
116
|
+
#
|
|
117
|
+
# A file applied in a single transaction (the default) is recorded by
|
|
118
|
+
# the same psql invocation, inside that transaction: the script and
|
|
119
|
+
# its record commit together or not at all, so no interruption can
|
|
120
|
+
# leave a script applied but unrecorded for the next run to repeat.
|
|
121
|
+
#
|
|
122
|
+
# A transaction = none file has no transaction to record it in. It is
|
|
123
|
+
# recorded afterwards on a connection of its own, and a failure of
|
|
124
|
+
# that record raises a ScriptError whose applied? is true: the script
|
|
125
|
+
# ran, and its dml is the one statement left to run.
|
|
126
|
+
#
|
|
127
|
+
# @param scripts The Scripts instance that records filename as run;
|
|
128
|
+
# a failure's ScriptError prints the command that records it there.
|
|
129
|
+
def psql_file(filename, path, scripts)
|
|
85
130
|
Preconditions.assert_class(path, String)
|
|
131
|
+
Preconditions.assert_class(scripts, Scripts)
|
|
86
132
|
Preconditions.check_state(File.exist?(path), "File[%s] not found" % path)
|
|
87
133
|
|
|
88
|
-
|
|
134
|
+
single = MigrationFile.new(path).single_transaction?
|
|
135
|
+
record_sql = scripts.record_as_run_sql(filename)
|
|
136
|
+
command = psql_file_command(path, single ? record_sql : nil)
|
|
89
137
|
|
|
90
138
|
output, status = Open3.capture2e(*command)
|
|
91
139
|
if !status.success?
|
|
92
|
-
raise ScriptError.new(
|
|
140
|
+
raise ScriptError.new(scripts, filename, path, output)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
if !single
|
|
144
|
+
begin
|
|
145
|
+
scripts.record_as_run!(filename)
|
|
146
|
+
rescue RuntimeError => e
|
|
147
|
+
raise ScriptError.new(scripts, filename, path, e.message, :applied => true)
|
|
148
|
+
end
|
|
93
149
|
end
|
|
94
150
|
end
|
|
95
151
|
|
|
96
152
|
# The psql argv psql_file runs. ON_ERROR_STOP is set on the
|
|
97
153
|
# command line, after any --set the caller passed, so it is in
|
|
98
154
|
# effect before the first line of the file is read.
|
|
99
|
-
|
|
100
|
-
|
|
155
|
+
#
|
|
156
|
+
# @param record_sql: When given, run by the same psql after the file
|
|
157
|
+
# (a --command after the --file), so --single-transaction wraps both
|
|
158
|
+
# in one BEGIN ... COMMIT.
|
|
159
|
+
def psql_file_command(path, record_sql=nil)
|
|
160
|
+
record = record_sql ? ["--command", record_sql] : []
|
|
161
|
+
@psql_args + PSQL_ISOLATION_OPTIONS + ["--set", "ON_ERROR_STOP=1", "--file", path] + record + Db.attribute_values(path) + [@url]
|
|
101
162
|
end
|
|
102
163
|
|
|
103
164
|
# True if the specific schema exists; false otherwise. Once seen the
|
|
@@ -111,26 +172,57 @@ module SchemaEvolutionManager
|
|
|
111
172
|
end
|
|
112
173
|
|
|
113
174
|
# Parses command line arguments returning an instance of
|
|
114
|
-
# Db.
|
|
175
|
+
# Db. Raises InvalidConnection if they name no database.
|
|
115
176
|
def Db.parse_command_line_config(arg_string)
|
|
116
177
|
Preconditions.assert_class(arg_string, String)
|
|
117
|
-
args = Args.new(arg_string, :optional =>
|
|
178
|
+
args = Args.new(arg_string, :optional => CONNECTION_FLAGS)
|
|
118
179
|
Db.from_args(args)
|
|
119
180
|
end
|
|
120
181
|
|
|
182
|
+
# The Db a command's connection flags name. Prompts for a password
|
|
183
|
+
# when --password was given. A connection the flags do not name is
|
|
184
|
+
# one ERROR line on stderr: exit RdocUsage::MISSING_INPUT when no
|
|
185
|
+
# database is named, RdocUsage::INVALID_INPUT when --url is malformed.
|
|
186
|
+
def Db.from_command_line(args)
|
|
187
|
+
Preconditions.assert_class(args, Args)
|
|
188
|
+
password = args.password ? Ask.for_password("Please enter your password: ") : nil
|
|
189
|
+
begin
|
|
190
|
+
Db.from_args(args, :password => password)
|
|
191
|
+
rescue MissingConnection => e
|
|
192
|
+
RdocUsage.error!(e.message, RdocUsage::MISSING_INPUT)
|
|
193
|
+
rescue InvalidConnection => e
|
|
194
|
+
RdocUsage.error!(e.message, RdocUsage::INVALID_INPUT)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
121
198
|
# @param password: Optional password to use when connecting to the database.
|
|
199
|
+
# Raises InvalidConnection unless args name a database: a --url
|
|
200
|
+
# that is a connection string, or a --name.
|
|
122
201
|
def Db.from_args(args, opts={})
|
|
123
202
|
Preconditions.assert_class(args, Args)
|
|
124
203
|
password = opts.delete(:password)
|
|
125
204
|
Preconditions.assert_empty_opts(opts)
|
|
126
205
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
206
|
+
Db.new(Db.connection_url(args), :password => password, :set => args.set)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# The url args name: --url as given, else one built from --host,
|
|
210
|
+
# --port, --name and --user
|
|
211
|
+
def Db.connection_url(args)
|
|
212
|
+
url = args.url.to_s.strip
|
|
213
|
+
name = args.name.to_s.strip
|
|
214
|
+
|
|
215
|
+
if !url.empty?
|
|
216
|
+
# A bare database name passed to --url is the common mistake
|
|
217
|
+
if !url.include?("://")
|
|
218
|
+
raise InvalidConnection.new("--url must be a full connection string (e.g. postgres://localhost:5432/%s). To connect by database name, use --name %s." % [url, url])
|
|
219
|
+
end
|
|
220
|
+
url
|
|
221
|
+
elsif name.empty?
|
|
222
|
+
raise MissingConnection.new("Missing database connection. Provide --url <postgres url> or --host/--name.")
|
|
130
223
|
else
|
|
131
|
-
base = "%s:%s/%s" % [args.host || "localhost", args.port || ConnectionData::DEFAULT_PORT,
|
|
132
|
-
|
|
133
|
-
Db.new("postgres://" + url, options)
|
|
224
|
+
base = "%s:%s/%s" % [args.host || "localhost", args.port || ConnectionData::DEFAULT_PORT, name]
|
|
225
|
+
"postgres://" + (args.user ? "%s@%s" % [args.user, base] : base)
|
|
134
226
|
end
|
|
135
227
|
end
|
|
136
228
|
|
|
@@ -38,14 +38,8 @@ module SchemaEvolutionManager
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def Library.git_assert_tag_exists(tag)
|
|
41
|
-
command = ["git", "tag", "-l"]
|
|
42
|
-
results = Library.system_or_error(command)
|
|
43
|
-
if results.nil?
|
|
44
|
-
raise "No git tags found"
|
|
45
|
-
end
|
|
46
|
-
|
|
47
41
|
if !Library.tag_exists?(tag)
|
|
48
|
-
raise "Tag[#{tag}] not found. Check
|
|
42
|
+
raise "Tag[#{tag}] not found. Check git tag -l"
|
|
49
43
|
end
|
|
50
44
|
end
|
|
51
45
|
|
|
@@ -61,7 +55,13 @@ module SchemaEvolutionManager
|
|
|
61
55
|
# no tags, otherwise returns an instance of Version. Only searches for
|
|
62
56
|
# tags matching x.x.x (e.g. 1.0.2)
|
|
63
57
|
def Library.latest_tag
|
|
64
|
-
Library.
|
|
58
|
+
Library.release_tags.last
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The release tags among +tags+ (x.y.z, optionally prefixed) as
|
|
62
|
+
# Versions, oldest first. Defaults to every tag in the repository.
|
|
63
|
+
def Library.release_tags(tags=Library.git_tags)
|
|
64
|
+
tags.select { |tag| Version.is_valid?(tag) }.map { |tag| Version.parse(tag) }.sort
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
# True only when a tag with exactly this name exists - 1.0.10 does
|
|
@@ -70,11 +70,14 @@ module SchemaEvolutionManager
|
|
|
70
70
|
Library.git_tags.include?(tag)
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
# Every tag in the repository. Raises when git fails (outside a
|
|
74
|
+
# checkout, git missing), so a failure never reads as "no tags".
|
|
73
75
|
def Library.git_tags
|
|
74
|
-
|
|
76
|
+
Library.system_or_error(["git", "tag", "-l"]).split
|
|
75
77
|
end
|
|
76
78
|
|
|
77
79
|
# Ex: Library.git_create_tag("0.0.1")
|
|
80
|
+
# Pushes only the tag it creates, never the checkout's other local tags.
|
|
78
81
|
def Library.git_create_tag(tag)
|
|
79
82
|
Library.assert_valid_tag(tag)
|
|
80
83
|
has_remote = Library.git_has_remote?
|
|
@@ -83,7 +86,7 @@ module SchemaEvolutionManager
|
|
|
83
86
|
end
|
|
84
87
|
Library.system_or_error(["git", "tag", "-a", "-m", tag, tag])
|
|
85
88
|
if has_remote
|
|
86
|
-
Library.system_or_error(["git", "push", "
|
|
89
|
+
Library.system_or_error(["git", "push", "origin", "refs/tags/%s" % tag])
|
|
87
90
|
end
|
|
88
91
|
end
|
|
89
92
|
|
|
@@ -53,6 +53,12 @@ module SchemaEvolutionManager
|
|
|
53
53
|
@attribute_values = parse_attribute_values
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
# True when psql applies the whole file in one transaction
|
|
57
|
+
# (transaction = single, the default)
|
|
58
|
+
def single_transaction?
|
|
59
|
+
@attribute_values.any? { |v| v.attribute.name == "transaction" && v.value == "single" }
|
|
60
|
+
end
|
|
61
|
+
|
|
56
62
|
private
|
|
57
63
|
|
|
58
64
|
# Returns a list of AttributeValues from the file itself,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module SchemaEvolutionManager
|
|
2
|
+
|
|
3
|
+
# Applies the pending scripts of a directory to the scripts table, with
|
|
4
|
+
# a dry run by default. Subclasses name the action (label) and whether
|
|
5
|
+
# each script body is executed (execute?); Scripts#apply_pending! is the
|
|
6
|
+
# loop.
|
|
7
|
+
class PendingScriptsUtil
|
|
8
|
+
|
|
9
|
+
def initialize(db, opts={})
|
|
10
|
+
@dry_run = opts.delete(:dry_run)
|
|
11
|
+
if @dry_run.nil?
|
|
12
|
+
@dry_run = true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
@db = Preconditions.assert_class(db, Db)
|
|
16
|
+
@scripts = Scripts.new(@db, Scripts::SCRIPTS)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def dry_run?
|
|
20
|
+
@dry_run
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Applies scripts in order, returning number of scripts applied
|
|
24
|
+
def apply!(dir)
|
|
25
|
+
@scripts.apply_pending!(dir, :execute => execute?, :dry_run => @dry_run, :label => label)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
@@ -26,6 +26,35 @@ module SchemaEvolutionManager
|
|
|
26
26
|
exit(exit_code)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# Exit codes for a command-line fault, one per kind of fault
|
|
30
|
+
unless defined?(MISSING_INPUT)
|
|
31
|
+
# A required argument, subcommand or connection was not given
|
|
32
|
+
MISSING_INPUT = 3
|
|
33
|
+
# An argument was given that the command does not accept
|
|
34
|
+
INVALID_INPUT = 4
|
|
35
|
+
# The input was well formed and running it failed
|
|
36
|
+
FAILED = 1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# A command-line fault: one "ERROR: <message>" line on stderr, then
|
|
40
|
+
# any detail lines (and the command's usage, with :usage => true),
|
|
41
|
+
# then exit with exit_code. Never writes to stdout, so a wrapper that
|
|
42
|
+
# captures stdout (TAG=$(sem-info tag next minor)) captures no error.
|
|
43
|
+
def RdocUsage.error!(message, exit_code, opts={})
|
|
44
|
+
details = opts.delete(:details) || []
|
|
45
|
+
usage = opts.delete(:usage)
|
|
46
|
+
Preconditions.assert_empty_opts(opts)
|
|
47
|
+
Preconditions.check_state([MISSING_INPUT, INVALID_INPUT, FAILED].include?(exit_code), "Invalid exit_code[%s]" % exit_code)
|
|
48
|
+
|
|
49
|
+
$stderr.puts "ERROR: %s" % message
|
|
50
|
+
details.each { |line| $stderr.puts line }
|
|
51
|
+
if usage
|
|
52
|
+
$stderr.puts ""
|
|
53
|
+
$stderr.puts RdocUsage.message
|
|
54
|
+
end
|
|
55
|
+
exit(exit_code)
|
|
56
|
+
end
|
|
57
|
+
|
|
29
58
|
private
|
|
30
59
|
def RdocUsage.program_name
|
|
31
60
|
$0
|
|
@@ -4,16 +4,31 @@ module SchemaEvolutionManager
|
|
|
4
4
|
|
|
5
5
|
attr_reader :path, :output
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
# @param scripts The Scripts instance that records filename as run
|
|
8
|
+
# @param applied: true when the script ran to completion and only
|
|
9
|
+
# recording it as run failed (a transaction = none script,
|
|
10
|
+
# whose record cannot share its transaction)
|
|
11
|
+
def initialize(scripts, filename, path, output, opts={})
|
|
12
|
+
@scripts = Preconditions.assert_class(scripts, Scripts)
|
|
9
13
|
@filename = Preconditions.assert_class(filename, String)
|
|
10
14
|
@path = Preconditions.assert_class(path, String)
|
|
11
15
|
@output = Preconditions.assert_class(output, String)
|
|
16
|
+
@applied = opts.delete(:applied) || false
|
|
17
|
+
Preconditions.assert_empty_opts(opts)
|
|
12
18
|
end
|
|
13
19
|
|
|
20
|
+
# True when the script was applied and is not recorded as run: its
|
|
21
|
+
# dml must be run before the next apply, or that apply runs it again
|
|
22
|
+
def applied?
|
|
23
|
+
@applied
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The psql command that records this script as applied: the statement
|
|
27
|
+
# Scripts#record_as_run! executes, in the argv Db#psql_command runs.
|
|
14
28
|
def dml
|
|
15
|
-
|
|
16
|
-
|
|
29
|
+
db = @scripts.db
|
|
30
|
+
command = db.psql_command_args(@scripts.record_as_run_sql(@filename)) + [db.sanitized_url]
|
|
31
|
+
Library.command_to_s(command)
|
|
17
32
|
end
|
|
18
33
|
|
|
19
34
|
end
|
|
@@ -2,6 +2,8 @@ module SchemaEvolutionManager
|
|
|
2
2
|
|
|
3
3
|
class Scripts
|
|
4
4
|
|
|
5
|
+
attr_reader :db, :table_name
|
|
6
|
+
|
|
5
7
|
if !defined?(SCRIPTS)
|
|
6
8
|
SCRIPTS = "scripts"
|
|
7
9
|
BOOTSTRAP_SCRIPTS = "bootstrap_scripts"
|
|
@@ -72,6 +74,45 @@ module SchemaEvolutionManager
|
|
|
72
74
|
end
|
|
73
75
|
end
|
|
74
76
|
|
|
77
|
+
# Walks the pending scripts of dir in order and records each as run,
|
|
78
|
+
# returning how many were pending.
|
|
79
|
+
#
|
|
80
|
+
# @param execute: true applies each script with Db#psql_file, which
|
|
81
|
+
# records it too (inside the script's own transaction unless it
|
|
82
|
+
# is transaction = none); false only records it (a baseline)
|
|
83
|
+
# @param dry_run: true prints each pending script and its path and
|
|
84
|
+
# changes nothing
|
|
85
|
+
# @param label: The action printed per script ("Applying",
|
|
86
|
+
# "Baselining", "Bootstrapping")
|
|
87
|
+
# @param quiet: true prints nothing outside a dry run
|
|
88
|
+
def apply_pending!(dir, opts={})
|
|
89
|
+
execute = opts.delete(:execute)
|
|
90
|
+
dry_run = opts.delete(:dry_run)
|
|
91
|
+
label = Preconditions.assert_class(opts.delete(:label), String)
|
|
92
|
+
quiet = opts.delete(:quiet) || false
|
|
93
|
+
Preconditions.assert_empty_opts(opts)
|
|
94
|
+
Preconditions.check_state(File.directory?(dir),
|
|
95
|
+
"Dir[%s] does not exist" % dir)
|
|
96
|
+
|
|
97
|
+
count = 0
|
|
98
|
+
each_pending(dir) do |filename, path|
|
|
99
|
+
count += 1
|
|
100
|
+
if dry_run
|
|
101
|
+
puts "[DRY RUN] #{label} #{filename}"
|
|
102
|
+
puts path
|
|
103
|
+
puts ""
|
|
104
|
+
elsif execute
|
|
105
|
+
print "#{label} #{filename}" unless quiet
|
|
106
|
+
@db.psql_file(filename, path, self)
|
|
107
|
+
puts " Done" unless quiet
|
|
108
|
+
else
|
|
109
|
+
puts "#{label} #{filename}" unless quiet
|
|
110
|
+
record_as_run!(filename)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
count
|
|
114
|
+
end
|
|
115
|
+
|
|
75
116
|
# True if this script has already been applied to the db. False
|
|
76
117
|
# otherwise.
|
|
77
118
|
def has_run?(filename)
|
|
@@ -86,9 +127,15 @@ module SchemaEvolutionManager
|
|
|
86
127
|
|
|
87
128
|
# Inserts a record to indiciate that we have loaded the specified file.
|
|
88
129
|
def record_as_run!(filename)
|
|
130
|
+
@db.psql_command(record_as_run_sql(filename))
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# The statement record_as_run! executes: an idempotent insert of
|
|
134
|
+
# filename into this instance's table. ScriptError#dml prints it as
|
|
135
|
+
# the recovery command, so the hint and the write are one statement.
|
|
136
|
+
def record_as_run_sql(filename)
|
|
89
137
|
literal = Db.quote_literal(Scripts.assert_valid_filename!(filename))
|
|
90
|
-
|
|
91
|
-
@db.psql_command(command)
|
|
138
|
+
"insert into %s.%s (filename) select %s where not exists (select 1 from %s.%s where filename = %s)" % [Db.schema_name, @table_name, literal, Db.schema_name, @table_name, literal]
|
|
92
139
|
end
|
|
93
140
|
|
|
94
141
|
# Returns the sorted list of filenames already applied to this database.
|
|
@@ -10,31 +10,16 @@ module SchemaEvolutionManager
|
|
|
10
10
|
valid = ['version', 'scripts']
|
|
11
11
|
subcommand = args.shift.to_s.strip
|
|
12
12
|
|
|
13
|
-
if
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
$stderr.puts "ERROR: Invalid db subcommand[%s]. Must be one of: %s" % [subcommand, valid.join(", ")]
|
|
18
|
-
end
|
|
19
|
-
exit(4)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
db_args = SchemaEvolutionManager::Args.new(args, :optional => ['url', 'host', 'user', 'name', 'port', 'set'])
|
|
23
|
-
|
|
24
|
-
if db_args.url.to_s.strip.empty? && db_args.name.to_s.strip.empty?
|
|
25
|
-
$stderr.puts "ERROR: Missing database connection. Provide --url <postgres url> or --host/--name."
|
|
26
|
-
exit(3)
|
|
13
|
+
if subcommand.empty?
|
|
14
|
+
RdocUsage.error!("Missing db subcommand. Must be one of: %s" % valid.join(", "), RdocUsage::MISSING_INPUT)
|
|
15
|
+
elsif !valid.include?(subcommand)
|
|
16
|
+
RdocUsage.error!("Invalid db subcommand[%s]. Must be one of: %s" % [subcommand, valid.join(", ")], RdocUsage::INVALID_INPUT)
|
|
27
17
|
end
|
|
28
18
|
|
|
29
|
-
|
|
30
|
-
# an actionable hint instead of letting parse_url raise a raw backtrace.
|
|
31
|
-
if db_args.url && !db_args.url.include?("://")
|
|
32
|
-
$stderr.puts "ERROR: --url must be a full connection string (e.g. postgres://localhost:5432/%s). To connect by database name, use --name %s." % [db_args.url, db_args.url]
|
|
33
|
-
exit(3)
|
|
34
|
-
end
|
|
19
|
+
db_args = SchemaEvolutionManager::Args.new(args, :optional => SchemaEvolutionManager::Db::CONNECTION_FLAGS)
|
|
35
20
|
|
|
36
21
|
begin
|
|
37
|
-
db = SchemaEvolutionManager::Db.
|
|
22
|
+
db = SchemaEvolutionManager::Db.from_command_line(db_args)
|
|
38
23
|
if subcommand == "version"
|
|
39
24
|
version = SchemaEvolutionManager::Versions.new(db).latest
|
|
40
25
|
if version.nil?
|
|
@@ -53,8 +38,7 @@ module SchemaEvolutionManager
|
|
|
53
38
|
rescue => e
|
|
54
39
|
# Surface a single clean line (e.g. bad url, unreachable host, missing
|
|
55
40
|
# database) rather than a Ruby stack trace.
|
|
56
|
-
|
|
57
|
-
exit(3)
|
|
41
|
+
RdocUsage.error!(e.message.to_s.split("\n").first, RdocUsage::FAILED)
|
|
58
42
|
end
|
|
59
43
|
end
|
|
60
44
|
|
|
@@ -66,8 +50,7 @@ module SchemaEvolutionManager
|
|
|
66
50
|
if subcommand == "exists"
|
|
67
51
|
tag = args.shift.to_s.strip
|
|
68
52
|
if tag.empty?
|
|
69
|
-
|
|
70
|
-
exit(3)
|
|
53
|
+
RdocUsage.error!("Missing tag.", RdocUsage::MISSING_INPUT)
|
|
71
54
|
elsif ::SchemaEvolutionManager::Library.tag_exists?(tag)
|
|
72
55
|
puts "true"
|
|
73
56
|
else
|
|
@@ -85,12 +68,10 @@ module SchemaEvolutionManager
|
|
|
85
68
|
::SchemaEvolutionManager::SemInfo::Tag.next(args).to_version_string
|
|
86
69
|
|
|
87
70
|
elsif subcommand.empty?
|
|
88
|
-
|
|
89
|
-
exit(3)
|
|
71
|
+
RdocUsage.error!("Missing tag subcommand. Must be one of: %s" % valid.join(", "), RdocUsage::MISSING_INPUT)
|
|
90
72
|
|
|
91
73
|
else
|
|
92
|
-
|
|
93
|
-
exit(4)
|
|
74
|
+
RdocUsage.error!("Invalid tag subcommand[%s]. Must be one of: %s" % [subcommand, valid.join(", ")], RdocUsage::INVALID_INPUT)
|
|
94
75
|
end
|
|
95
76
|
end
|
|
96
77
|
|
|
@@ -112,8 +93,7 @@ module SchemaEvolutionManager
|
|
|
112
93
|
if valid.include?(component)
|
|
113
94
|
latest.send("next_%s" % component)
|
|
114
95
|
else
|
|
115
|
-
|
|
116
|
-
exit(4)
|
|
96
|
+
RdocUsage.error!("Invalid component[%s]. Must be one of: %s" % [component, valid.join(", ")], RdocUsage::INVALID_INPUT)
|
|
117
97
|
end
|
|
118
98
|
end
|
|
119
99
|
|
|
@@ -2,7 +2,13 @@ module SchemaEvolutionManager
|
|
|
2
2
|
|
|
3
3
|
class Version
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
unless defined?(VERSION_FILE)
|
|
6
|
+
VERSION_FILE = File.join(Library.base_dir, "VERSION")
|
|
7
|
+
# An optional prefix (v1.2.3, r1.2.3) then major.minor.micro. The
|
|
8
|
+
# prefix is lazy so it never takes a leading digit of the major
|
|
9
|
+
# version: 10.2.3 is major 10, not prefix "1" and major 0.
|
|
10
|
+
PATTERN = /\A(\w*?)(\d+)\.(\d+)\.(\d+)\z/
|
|
11
|
+
end
|
|
6
12
|
|
|
7
13
|
attr_reader :major, :minor, :micro
|
|
8
14
|
|
|
@@ -11,15 +17,13 @@ module SchemaEvolutionManager
|
|
|
11
17
|
@minor = minor.to_i
|
|
12
18
|
@micro = micro.to_i
|
|
13
19
|
@prefix = opts.delete(:prefix) || nil
|
|
14
|
-
|
|
15
|
-
raise "Invalid keys: " + opts.keys
|
|
16
|
-
end
|
|
20
|
+
Preconditions.assert_empty_opts(opts)
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
def Version.parse(version_string)
|
|
20
24
|
Preconditions.check_not_blank(version_string, "version_string cannot be blank")
|
|
21
25
|
Library.assert_valid_tag(version_string)
|
|
22
|
-
if md = version_string.match(
|
|
26
|
+
if md = version_string.match(PATTERN)
|
|
23
27
|
Version.new(md[2], md[3], md[4], :prefix => md[1])
|
|
24
28
|
else
|
|
25
29
|
raise "ERROR: Bug in version string parser for version[%s]" % version_string
|
|
@@ -74,7 +78,7 @@ module SchemaEvolutionManager
|
|
|
74
78
|
|
|
75
79
|
def Version.is_valid?(version_string)
|
|
76
80
|
Preconditions.check_not_blank(version_string, "version_string cannot be blank")
|
|
77
|
-
|
|
81
|
+
PATTERN.match?(version_string)
|
|
78
82
|
end
|
|
79
83
|
|
|
80
84
|
end
|
|
@@ -23,6 +23,7 @@ load File.join(lib_dir, 'scripts.rb')
|
|
|
23
23
|
load File.join(lib_dir, 'versions.rb')
|
|
24
24
|
load File.join(lib_dir, 'connection_data.rb')
|
|
25
25
|
load File.join(lib_dir, 'db.rb')
|
|
26
|
+
load File.join(lib_dir, 'pending_scripts_util.rb')
|
|
26
27
|
load File.join(lib_dir, 'apply_util.rb')
|
|
27
28
|
load File.join(lib_dir, 'baseline_util.rb')
|
|
28
29
|
load File.join(lib_dir, 'template.rb')
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: schema-evolution-manager
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.
|
|
4
|
+
version: 0.9.75
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michael Bryzek
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-11-
|
|
10
|
+
date: 2026-11-11 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: '["Michael Bryzek"]'
|
|
13
13
|
email: mbryzek@alum.mit.edu
|
|
@@ -41,6 +41,7 @@ files:
|
|
|
41
41
|
- lib/schema-evolution-manager/install_template.rb
|
|
42
42
|
- lib/schema-evolution-manager/library.rb
|
|
43
43
|
- lib/schema-evolution-manager/migration_file.rb
|
|
44
|
+
- lib/schema-evolution-manager/pending_scripts_util.rb
|
|
44
45
|
- lib/schema-evolution-manager/preconditions.rb
|
|
45
46
|
- lib/schema-evolution-manager/rdoc_usage.rb
|
|
46
47
|
- lib/schema-evolution-manager/script_error.rb
|