schema-evolution-manager 0.9.58 → 0.9.61
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 +48 -3
- data/bin/sem-add +5 -4
- data/bin/sem-apply +3 -2
- data/bin/sem-baseline +3 -2
- data/bin/sem-dist +13 -10
- data/bin/sem-init +37 -15
- data/lib/schema-evolution-manager/args.rb +20 -7
- data/lib/schema-evolution-manager/ask.rb +1 -1
- data/lib/schema-evolution-manager/config.rb +90 -0
- data/lib/schema-evolution-manager/connection_data.rb +71 -18
- data/lib/schema-evolution-manager/db.rb +55 -60
- data/lib/schema-evolution-manager/install_template.rb +16 -13
- data/lib/schema-evolution-manager/library.rb +78 -24
- data/lib/schema-evolution-manager/migration_file.rb +3 -5
- data/lib/schema-evolution-manager/scripts.rb +19 -6
- data/lib/schema-evolution-manager/sem_info.rb +36 -10
- data/lib/schema-evolution-manager/sem_version.rb +1 -1
- data/lib/schema-evolution-manager/template.rb +3 -1
- data/lib/schema-evolution-manager.rb +4 -1
- data/template/dev.rb +3 -3
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 60215e02faf6492f2a4e0b263008b6038f95d3ff824417fb6ce615296342ea5f
|
|
4
|
+
data.tar.gz: e9450f3c24a2f54b3f25566c1152a6454de7e7b2eb4750dd0a8021b273b014c6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6673d52ee37258fee586cd320799eeff9e6477d963a98c79f737f1fc8f44009dcbc076ed26dc8e900ef970e79516d1d115d125b6ec4d23da45d1b5903508ca8b
|
|
7
|
+
data.tar.gz: 3081446f6663a18bf00523332c0d9363e9b3322c9a900561205e1d90509698f4a6899824087a202ba9fdc5f6487fd9ab475574fbca246c7c0e156f691ceb700a
|
data/README.md
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
[](https://travis-ci.org/mbryzek/schema-evolution-manager)
|
|
2
|
-
|
|
3
1
|
# Schema Evolution Manager (sem)
|
|
4
2
|
|
|
5
3
|
## Intended Audience
|
|
@@ -43,6 +41,24 @@ dozens of independent postgresql databases.
|
|
|
43
41
|
See INSTALLATION and GETTING STARTED for details.
|
|
44
42
|
|
|
45
43
|
|
|
44
|
+
## Why sem
|
|
45
|
+
|
|
46
|
+
sem is deliberately small. A schema change is a plain SQL file, named
|
|
47
|
+
by the timestamp it was added, committed to git next to every other
|
|
48
|
+
change to that database. There is no migration DSL, no down
|
|
49
|
+
migrations, and no runtime dependency beyond ruby, git and psql.
|
|
50
|
+
|
|
51
|
+
Applying changes is psql running each file in order inside a
|
|
52
|
+
transaction, and recording the filename in one table. Because psql
|
|
53
|
+
does the work, anything that runs in psql runs in sem: plpgsql,
|
|
54
|
+
extensions, data fixes, large backfills.
|
|
55
|
+
|
|
56
|
+
If you want a tool that generates SQL from a model, or that rolls
|
|
57
|
+
schema changes back for you, sem is the wrong choice. If you want the
|
|
58
|
+
SQL you wrote to be the SQL that runs, and a plain record of what ran
|
|
59
|
+
where, it is a good fit.
|
|
60
|
+
|
|
61
|
+
|
|
46
62
|
## Project Goals
|
|
47
63
|
|
|
48
64
|
- Absolutely minimal set of dependencies. We found that anything
|
|
@@ -133,7 +149,7 @@ There are three ways to install schema evolution manager:
|
|
|
133
149
|
|
|
134
150
|
git clone git@github.com:mbryzek/schema-evolution-manager.git
|
|
135
151
|
cd schema-evolution-manager
|
|
136
|
-
git checkout 0.9.
|
|
152
|
+
git checkout 0.9.61
|
|
137
153
|
ruby ./configure.rb
|
|
138
154
|
sudo ./install.rb
|
|
139
155
|
|
|
@@ -157,6 +173,23 @@ version.
|
|
|
157
173
|
git init /tmp/sample
|
|
158
174
|
sem-init --dir /tmp/sample --url postgresql://postgres@localhost/sample
|
|
159
175
|
|
|
176
|
+
### Configuring the scripts directory (.sem)
|
|
177
|
+
|
|
178
|
+
By default sem keeps scripts in ./scripts. To keep them elsewhere, commit
|
|
179
|
+
a file named .sem at the root of the repository:
|
|
180
|
+
|
|
181
|
+
sem.config.scripts_dir = schema/scripts
|
|
182
|
+
|
|
183
|
+
Every command walks up from the current directory to the first .sem and
|
|
184
|
+
resolves paths relative to the directory that holds it. Blank lines and
|
|
185
|
+
lines starting with # are ignored. sem-init writes the file for you when
|
|
186
|
+
given --scripts_dir:
|
|
187
|
+
|
|
188
|
+
sem-init --dir /tmp/sample --url postgresql://postgres@localhost/sample --scripts_dir schema/scripts
|
|
189
|
+
|
|
190
|
+
sem-dist always packs the scripts under the name scripts/ and never packs
|
|
191
|
+
.sem, so a tarball applies on a server with no .sem.
|
|
192
|
+
|
|
160
193
|
### Writing your first sql script
|
|
161
194
|
|
|
162
195
|
cd /tmp/sample
|
|
@@ -236,6 +269,18 @@ There are two recommended ways in which to pass user passwords to psql:
|
|
|
236
269
|
|
|
237
270
|
sem-apply --url postgresql://postgres@localhost/sample --password
|
|
238
271
|
|
|
272
|
+
### Running inside the postgres Docker image
|
|
273
|
+
|
|
274
|
+
The official postgres image runs the scripts in
|
|
275
|
+
docker-entrypoint-initdb.d before the server listens on TCP. During
|
|
276
|
+
that phase postgres accepts connections only on its unix socket, so a
|
|
277
|
+
URL naming localhost is refused. Leave the host out of the URL and
|
|
278
|
+
libpq connects over the socket:
|
|
279
|
+
|
|
280
|
+
sem-apply --url postgresql://$POSTGRES_USER@/$POSTGRES_DB
|
|
281
|
+
|
|
282
|
+
No password is needed on the socket during initialization.
|
|
283
|
+
|
|
239
284
|
### Apply the changes
|
|
240
285
|
|
|
241
286
|
sem-apply --url postgresql://postgres@localhost/sample
|
data/bin/sem-add
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
-
# == Adds a database upgrade script to this repository.
|
|
2
|
+
# == Adds a database upgrade script to this repository. The script is
|
|
3
|
+
# written to the scripts directory named by .sem (default ./scripts)
|
|
3
4
|
#
|
|
4
5
|
# == Usage
|
|
5
6
|
# sem-add <path>
|
|
@@ -19,7 +20,7 @@ end
|
|
|
19
20
|
SchemaEvolutionManager::Preconditions.check_state(File.exist?(file), "File[#{file}] could not be found")
|
|
20
21
|
SchemaEvolutionManager::Preconditions.check_state(file.match(/\.sql/i), "File[#{file}] must end with .sql")
|
|
21
22
|
|
|
22
|
-
scripts_dir =
|
|
23
|
+
scripts_dir = SchemaEvolutionManager::Config.load.scripts_dir
|
|
23
24
|
SchemaEvolutionManager::Library.ensure_dir!(scripts_dir)
|
|
24
25
|
|
|
25
26
|
contents = IO.read(file)
|
|
@@ -33,7 +34,7 @@ while File.exist?(target)
|
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
puts "Adding #{target}"
|
|
36
|
-
SchemaEvolutionManager::Library.system_or_error("mv
|
|
37
|
-
SchemaEvolutionManager::Library.system_or_error("git add
|
|
37
|
+
SchemaEvolutionManager::Library.system_or_error(["mv", "--", file, target])
|
|
38
|
+
SchemaEvolutionManager::Library.system_or_error(["git", "add", "--", target])
|
|
38
39
|
|
|
39
40
|
puts "File staged in git. You need to commit and push"
|
data/bin/sem-apply
CHANGED
|
@@ -23,6 +23,7 @@ password = if args.password
|
|
|
23
23
|
nil
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
config = SchemaEvolutionManager::Config.load
|
|
26
27
|
db = SchemaEvolutionManager::Db.from_args(args, :password => password)
|
|
27
28
|
util = SchemaEvolutionManager::ApplyUtil.new(db, :dry_run => args.dry_run || false)
|
|
28
29
|
|
|
@@ -30,12 +31,12 @@ begin
|
|
|
30
31
|
db.bootstrap!
|
|
31
32
|
|
|
32
33
|
puts "Upgrading schema for #{db.sanitized_url}"
|
|
33
|
-
count = util.apply!(
|
|
34
|
+
count = util.apply!(config.scripts_dir)
|
|
34
35
|
if count == 0
|
|
35
36
|
puts " All scripts have been previously applied"
|
|
36
37
|
end
|
|
37
38
|
|
|
38
|
-
version_file = "
|
|
39
|
+
version_file = File.join(config.base_dir, "VERSION")
|
|
39
40
|
if !util.dry_run? && File.exist?(version_file)
|
|
40
41
|
version = IO.read(version_file).strip
|
|
41
42
|
if !version.empty?
|
data/bin/sem-baseline
CHANGED
|
@@ -17,6 +17,7 @@ load File.join(File.dirname(__FILE__), 'sem-config')
|
|
|
17
17
|
|
|
18
18
|
args = SchemaEvolutionManager::Args.from_stdin(:optional => %w(url host port name user dry_run))
|
|
19
19
|
|
|
20
|
+
config = SchemaEvolutionManager::Config.load
|
|
20
21
|
db = SchemaEvolutionManager::Db.from_args(args)
|
|
21
22
|
db.bootstrap!
|
|
22
23
|
|
|
@@ -24,10 +25,10 @@ dry_run = args.dry_run.nil? ? false : args.dry_run
|
|
|
24
25
|
|
|
25
26
|
util = SchemaEvolutionManager::BaselineUtil.new(db, :dry_run => dry_run)
|
|
26
27
|
|
|
27
|
-
puts "Baselining schema for #{db.
|
|
28
|
+
puts "Baselining schema for #{db.sanitized_url}"
|
|
28
29
|
|
|
29
30
|
begin
|
|
30
|
-
count = util.apply!(
|
|
31
|
+
count = util.apply!(config.scripts_dir)
|
|
31
32
|
if count == 0
|
|
32
33
|
puts " All scripts have been previously applied"
|
|
33
34
|
end
|
data/bin/sem-dist
CHANGED
|
@@ -30,13 +30,13 @@ args = SchemaEvolutionManager::Args.from_stdin(:optional => ['artifact_name', 't
|
|
|
30
30
|
|
|
31
31
|
# On MAC OS X, use gnutar if available, otherwise use tar with --no-xattrs
|
|
32
32
|
tar_cmd = `which gnutar 2> /dev/null`.strip
|
|
33
|
-
tar_opts =
|
|
33
|
+
tar_opts = []
|
|
34
34
|
if tar_cmd == ""
|
|
35
35
|
tar_cmd = "tar"
|
|
36
36
|
# Use --no-xattrs to prevent extended attributes from being archived (works in create mode)
|
|
37
37
|
# Also use --no-mac-metadata to prevent AppleDouble files
|
|
38
|
-
if system("tar --no-xattrs -cf /dev/null /dev/null
|
|
39
|
-
tar_opts = "--no-xattrs --no-mac-metadata"
|
|
38
|
+
if system("tar", "--no-xattrs", "-cf", "/dev/null", "/dev/null", :err => File::NULL)
|
|
39
|
+
tar_opts = ["--no-xattrs", "--no-mac-metadata"]
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -58,7 +58,8 @@ SchemaEvolutionManager::Library.git_assert_tag_exists(tag)
|
|
|
58
58
|
SchemaEvolutionManager::Versions.validate_version!(tag)
|
|
59
59
|
|
|
60
60
|
changes = SchemaEvolutionManager::Library.git_changes(:tag => tag)
|
|
61
|
-
|
|
61
|
+
config = SchemaEvolutionManager::Config.load
|
|
62
|
+
repo_path = config.base_dir
|
|
62
63
|
|
|
63
64
|
if args.artifact_name
|
|
64
65
|
artifact_name = args.artifact_name
|
|
@@ -76,16 +77,19 @@ SchemaEvolutionManager::Library.with_temp_file do |tmp|
|
|
|
76
77
|
tmpdir = File.join(tmp, filename)
|
|
77
78
|
SchemaEvolutionManager::Library.ensure_dir!(tmpdir)
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
# The tarball always holds the scripts under the fixed name scripts/
|
|
81
|
+
# and never .sem, so a server applies it without any config.
|
|
82
|
+
SchemaEvolutionManager::Library.system_or_error(["cp", "-R", "--", config.scripts_dir, File.join(tmpdir, "scripts")])
|
|
80
83
|
File.open(File.join(tmpdir, "CHANGES"), "w") { |out| out << changes }
|
|
81
84
|
File.open(File.join(tmpdir, "VERSION"), "w") { |out| out << tag }
|
|
82
85
|
|
|
83
|
-
# Clear extended attributes to avoid tar warnings on Linux
|
|
84
|
-
|
|
86
|
+
# Clear extended attributes to avoid tar warnings on Linux. Best
|
|
87
|
+
# effort: xattr is absent on most Linux hosts.
|
|
88
|
+
system("xattr", "-cr", "--", tmpdir, [:out, :err] => File::NULL)
|
|
85
89
|
|
|
86
90
|
Dir.chdir(tmp) do
|
|
87
91
|
# COPYFILE_DISABLE prevents AppleDouble ._ files from being created
|
|
88
|
-
SchemaEvolutionManager::Library.system_or_error(
|
|
92
|
+
SchemaEvolutionManager::Library.system_or_error([tar_cmd] + tar_opts + ["-cf", "#{filename}.tar", "--", filename], :env => { "COPYFILE_DISABLE" => "1" })
|
|
89
93
|
FileUtils.cp("#{filename}.tar", tarball)
|
|
90
94
|
end
|
|
91
95
|
end
|
|
@@ -96,8 +100,7 @@ if File.exist?(gzip_file)
|
|
|
96
100
|
FileUtils.rm(gzip_file)
|
|
97
101
|
end
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
SchemaEvolutionManager::Library.system_or_error(command)
|
|
103
|
+
SchemaEvolutionManager::Library.system_or_error(["gzip", "--", tarball])
|
|
101
104
|
|
|
102
105
|
puts ""
|
|
103
106
|
puts "Created distribution file at #{gzip_file}"
|
data/bin/sem-init
CHANGED
|
@@ -9,16 +9,19 @@
|
|
|
9
9
|
#
|
|
10
10
|
# dir: The directory containing a git repository that will contain the schema evolution files
|
|
11
11
|
# url: The connection string for the psql database
|
|
12
|
+
# scripts_dir: Optional path to the scripts directory, relative to dir. When given,
|
|
13
|
+
# it is recorded in dir/.sem so every sem command finds it. Defaults to scripts
|
|
12
14
|
#
|
|
13
15
|
# == Example
|
|
14
16
|
# git init /tmp/example_repo
|
|
15
17
|
# bin/sem-init --dir /tmp/example_repo --url postgresql://username@localhost/sample
|
|
18
|
+
# bin/sem-init --dir /tmp/example_repo --url postgresql://username@localhost/sample --scripts_dir schema/scripts
|
|
16
19
|
#
|
|
17
20
|
|
|
18
21
|
load File.join(File.dirname(__FILE__), 'sem-config')
|
|
19
22
|
SchemaEvolutionManager::Library.set_verbose(true)
|
|
20
23
|
|
|
21
|
-
args = SchemaEvolutionManager::Args.from_stdin(:required => %w(dir), :optional => %w(url host port name user))
|
|
24
|
+
args = SchemaEvolutionManager::Args.from_stdin(:required => %w(dir), :optional => %w(url host port name user scripts_dir))
|
|
22
25
|
SchemaEvolutionManager::Preconditions.check_state(File.directory?(args.dir), "Dir[%s] does not exist" % args.dir)
|
|
23
26
|
|
|
24
27
|
if args.url.nil? && args.name.nil?
|
|
@@ -43,8 +46,11 @@ template_dir = File.join(SchemaEvolutionManager::Library.base_dir, "template")
|
|
|
43
46
|
SchemaEvolutionManager::Preconditions.check_state(File.directory?(template_dir),
|
|
44
47
|
"Could not find schema-evolution-manager/template subdir. Expected it at[%s]" % [template_dir])
|
|
45
48
|
|
|
49
|
+
# The wrapper scripts are committed to the repository, so they carry the url
|
|
50
|
+
# with any password removed; a password is supplied through a pgpass file.
|
|
51
|
+
# dev.rb takes the url as a ruby string literal.
|
|
46
52
|
subs = {
|
|
47
|
-
"
|
|
53
|
+
"url_literal" => SchemaEvolutionManager::ConnectionData.strip_password(db.url).inspect,
|
|
48
54
|
"add_script_path" => File.join(SchemaEvolutionManager::Library.base_dir, "bin/sem-add")
|
|
49
55
|
}
|
|
50
56
|
|
|
@@ -57,40 +63,56 @@ Dir.chdir(args.dir) do
|
|
|
57
63
|
if !File.exist?(wrapper)
|
|
58
64
|
puts "Creating wrapper script #{wrapper}"
|
|
59
65
|
copy_file(path, wrapper, subs)
|
|
60
|
-
SchemaEvolutionManager::Library.system_or_error("chmod +x
|
|
61
|
-
SchemaEvolutionManager::Library.system_or_error("git add
|
|
66
|
+
SchemaEvolutionManager::Library.system_or_error(["chmod", "--", "+x", wrapper])
|
|
67
|
+
SchemaEvolutionManager::Library.system_or_error(["git", "add", "--", wrapper])
|
|
62
68
|
wrappers << wrapper
|
|
63
69
|
end
|
|
64
70
|
end
|
|
65
71
|
if !wrappers.empty?
|
|
66
|
-
SchemaEvolutionManager::Library.system_or_error("git commit -m
|
|
72
|
+
SchemaEvolutionManager::Library.system_or_error(["git", "commit", "-m", "Add schema-evolution-manager wrapper scripts", "--"] + wrappers)
|
|
67
73
|
end
|
|
68
74
|
|
|
69
75
|
readme = "README.md"
|
|
70
76
|
if !File.exist?(readme)
|
|
71
77
|
puts "Creating #{readme}"
|
|
72
78
|
copy_file("schema-evolution-manager/template/README.md", "README.md", subs)
|
|
73
|
-
SchemaEvolutionManager::Library.system_or_error("git add
|
|
74
|
-
SchemaEvolutionManager::Library.system_or_error("git commit -m
|
|
79
|
+
SchemaEvolutionManager::Library.system_or_error(["git", "add", "--", readme])
|
|
80
|
+
SchemaEvolutionManager::Library.system_or_error(["git", "commit", "-m", "Adding README.md", "--", readme])
|
|
75
81
|
end
|
|
76
82
|
|
|
77
|
-
if
|
|
78
|
-
SchemaEvolutionManager::
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
if args.scripts_dir
|
|
84
|
+
config_file = SchemaEvolutionManager::Config::FILENAME
|
|
85
|
+
if File.exist?(config_file)
|
|
86
|
+
existing = SchemaEvolutionManager::Config.parse(File.expand_path(config_file))
|
|
87
|
+
SchemaEvolutionManager::Preconditions.check_state(existing.scripts_dir == File.expand_path(args.scripts_dir),
|
|
88
|
+
"%s already sets scripts_dir[%s] - not overwriting with[%s]" % [config_file, existing.scripts_dir, args.scripts_dir])
|
|
89
|
+
else
|
|
90
|
+
puts "Creating #{config_file}"
|
|
91
|
+
File.open(config_file, "w") { |out| out << "sem.config.scripts_dir = #{args.scripts_dir}\n" }
|
|
92
|
+
SchemaEvolutionManager::Library.system_or_error(["git", "add", "--", config_file])
|
|
93
|
+
SchemaEvolutionManager::Library.system_or_error(["git", "commit", "-m", "Adding #{config_file}", "--", config_file])
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
scripts_dir = args.scripts_dir || "scripts"
|
|
98
|
+
if !File.exist?(scripts_dir)
|
|
99
|
+
exists_marker = File.join(scripts_dir, ".exists")
|
|
100
|
+
SchemaEvolutionManager::Library.ensure_dir!(scripts_dir)
|
|
101
|
+
SchemaEvolutionManager::Library.system_or_error(["touch", exists_marker])
|
|
102
|
+
SchemaEvolutionManager::Library.system_or_error(["git", "add", "--", exists_marker])
|
|
103
|
+
SchemaEvolutionManager::Library.system_or_error(["git", "commit", "-m", "Adding scripts directory", "--", exists_marker])
|
|
82
104
|
end
|
|
83
105
|
|
|
84
106
|
# Create the first git tag if necessary
|
|
85
|
-
tags = SchemaEvolutionManager::Library.system_or_error("git tag -l")
|
|
107
|
+
tags = SchemaEvolutionManager::Library.system_or_error(["git", "tag", "-l"])
|
|
86
108
|
if tags.nil? || tags == ""
|
|
87
109
|
puts "Creating initial git tag (0.0.1)"
|
|
88
110
|
SchemaEvolutionManager::Library.git_create_tag("0.0.1")
|
|
89
111
|
end
|
|
90
112
|
|
|
91
113
|
if SchemaEvolutionManager::Library.git_has_remote?
|
|
92
|
-
branch = SchemaEvolutionManager::Library.system_or_error("git symbolic-ref --short HEAD")
|
|
93
|
-
SchemaEvolutionManager::Library.system_or_error("git push origin
|
|
114
|
+
branch = SchemaEvolutionManager::Library.system_or_error(["git", "symbolic-ref", "--short", "HEAD"])
|
|
115
|
+
SchemaEvolutionManager::Library.system_or_error(["git", "push", "origin", branch])
|
|
94
116
|
end
|
|
95
117
|
|
|
96
118
|
end
|
|
@@ -16,6 +16,7 @@ module SchemaEvolutionManager
|
|
|
16
16
|
:name => "Specifies the name of the database to which to connect",
|
|
17
17
|
:url => "The connection string for the psql database",
|
|
18
18
|
:dir => "Path to a directory",
|
|
19
|
+
:scripts_dir => "Path to the scripts directory, relative to --dir. Recorded in .sem",
|
|
19
20
|
:tag => "A git tag (e.g. 0.0.1)",
|
|
20
21
|
:prefix => "Configure installer to use this prefix",
|
|
21
22
|
:set => "Passthrough for postgresql --set argument. Returns an array of the options set"
|
|
@@ -26,17 +27,21 @@ module SchemaEvolutionManager
|
|
|
26
27
|
:dry_run => "Include flag to echo commands that will run without actually executing them",
|
|
27
28
|
:non_interactive => "Avoid all prompts and use defaults for any option that requires user input",
|
|
28
29
|
:help => "Display help",
|
|
30
|
+
:version => "Display the version of schema-evolution-manager",
|
|
29
31
|
:verbose => "Enable verbose logging of all system calls",
|
|
30
32
|
}
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
attr_reader :artifact_name, :host, :port, :name, :prefix, :url, :user, :dir, :dry_run, :non_interactive, :tag, :password, :set
|
|
35
|
+
attr_reader :artifact_name, :host, :port, :name, :prefix, :url, :user, :dir, :dry_run, :non_interactive, :tag, :password, :set, :scripts_dir
|
|
34
36
|
|
|
35
|
-
# args:
|
|
37
|
+
# args: The arguments, either an argv array (each element one
|
|
38
|
+
# argument, so a value may contain whitespace) or a string
|
|
39
|
+
# split on whitespace
|
|
36
40
|
# :required => list of parameters that are required
|
|
37
41
|
# :optional => list of parameters that are optional
|
|
38
42
|
def initialize(args, opts={})
|
|
39
|
-
Preconditions.
|
|
43
|
+
Preconditions.check_state(args.nil? || args.is_a?(String) || args.is_a?(Array),
|
|
44
|
+
"args must be a String or an Array")
|
|
40
45
|
required = (opts.delete(:required) || []).map { |flag| format_flag(flag) }
|
|
41
46
|
optional = (opts.delete(:optional) || []).map { |flag| format_flag(flag) }
|
|
42
47
|
Preconditions.assert_class(required, Array)
|
|
@@ -51,6 +56,9 @@ module SchemaEvolutionManager
|
|
|
51
56
|
if !optional.include?(:verbose)
|
|
52
57
|
optional << :verbose
|
|
53
58
|
end
|
|
59
|
+
if !optional.include?(:version)
|
|
60
|
+
optional << :version
|
|
61
|
+
end
|
|
54
62
|
|
|
55
63
|
found_arguments = parse_string_arguments(args)
|
|
56
64
|
missing = required.select { |field| blank?(found_arguments[field]) }
|
|
@@ -63,6 +71,7 @@ module SchemaEvolutionManager
|
|
|
63
71
|
@url = found_arguments.delete(:url)
|
|
64
72
|
@user = found_arguments.delete(:user)
|
|
65
73
|
@dir = found_arguments.delete(:dir)
|
|
74
|
+
@scripts_dir = found_arguments.delete(:scripts_dir)
|
|
66
75
|
@tag = found_arguments.delete(:tag)
|
|
67
76
|
@set = found_arguments.delete(:set) || []
|
|
68
77
|
|
|
@@ -71,6 +80,7 @@ module SchemaEvolutionManager
|
|
|
71
80
|
@password = found_arguments.delete(:password)
|
|
72
81
|
@help = found_arguments.delete(:help)
|
|
73
82
|
@verbose = found_arguments.delete(:verbose)
|
|
83
|
+
@version = found_arguments.delete(:version)
|
|
74
84
|
|
|
75
85
|
Preconditions.check_state(found_arguments.empty?,
|
|
76
86
|
"Did not handle all flags: %s" % found_arguments.keys.join(" "))
|
|
@@ -79,6 +89,11 @@ module SchemaEvolutionManager
|
|
|
79
89
|
RdocUsage.printAndExit(0)
|
|
80
90
|
end
|
|
81
91
|
|
|
92
|
+
if @version
|
|
93
|
+
puts SemVersion::VERSION
|
|
94
|
+
exit(0)
|
|
95
|
+
end
|
|
96
|
+
|
|
82
97
|
if @verbose
|
|
83
98
|
Library.set_verbose(true)
|
|
84
99
|
end
|
|
@@ -90,8 +105,7 @@ module SchemaEvolutionManager
|
|
|
90
105
|
|
|
91
106
|
# Hack to minimize bleeding from STDIN. Returns an instance of Args class
|
|
92
107
|
def Args.from_stdin(opts)
|
|
93
|
-
|
|
94
|
-
Args.new(values, opts)
|
|
108
|
+
Args.new(ARGV.dup, opts)
|
|
95
109
|
end
|
|
96
110
|
|
|
97
111
|
private
|
|
@@ -133,10 +147,9 @@ module SchemaEvolutionManager
|
|
|
133
147
|
|
|
134
148
|
|
|
135
149
|
def parse_string_arguments(args)
|
|
136
|
-
Preconditions.assert_class_or_nil(args, String)
|
|
137
150
|
found = {}
|
|
138
151
|
index = 0
|
|
139
|
-
values = args.to_s.strip.split(/\s+/)
|
|
152
|
+
values = args.is_a?(Array) ? args : args.to_s.strip.split(/\s+/)
|
|
140
153
|
while index < values.length do
|
|
141
154
|
flag = format_flag(values[index])
|
|
142
155
|
index += 1
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module SchemaEvolutionManager
|
|
2
|
+
|
|
3
|
+
# Repository level configuration, read from a file named .sem. Every
|
|
4
|
+
# command walks up from the current directory to the first .sem; the
|
|
5
|
+
# directory holding it is the base directory. Lines follow the
|
|
6
|
+
# migration file attribute grammar:
|
|
7
|
+
#
|
|
8
|
+
# sem.config.scripts_dir = schema/scripts
|
|
9
|
+
#
|
|
10
|
+
# Blank lines and lines starting with # are ignored. Paths are
|
|
11
|
+
# relative to the base directory. With no .sem anywhere, the base
|
|
12
|
+
# directory is the start directory and scripts_dir is ./scripts.
|
|
13
|
+
class Config
|
|
14
|
+
|
|
15
|
+
unless defined?(FILENAME)
|
|
16
|
+
FILENAME = ".sem"
|
|
17
|
+
PROPERTY_PREFIX = /^sem\.config\./
|
|
18
|
+
DEFAULT_SCRIPTS_DIR = "scripts"
|
|
19
|
+
NAMES = ["scripts_dir"]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :path, :base_dir, :scripts_dir
|
|
23
|
+
|
|
24
|
+
# path: the .sem file this config was read from, or nil
|
|
25
|
+
# base_dir: absolute path against which relative paths resolve
|
|
26
|
+
# scripts_dir: scripts directory, relative to base_dir or absolute
|
|
27
|
+
def initialize(path, base_dir, scripts_dir)
|
|
28
|
+
@path = path
|
|
29
|
+
@base_dir = Library.normalize_path(File.expand_path(base_dir))
|
|
30
|
+
@scripts_dir = Library.normalize_path(File.expand_path(scripts_dir, @base_dir))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def Config.load(start_dir=Dir.pwd)
|
|
34
|
+
Preconditions.assert_class(start_dir, String)
|
|
35
|
+
start = File.expand_path(start_dir)
|
|
36
|
+
Preconditions.check_state(File.directory?(start), "Dir[%s] does not exist" % start)
|
|
37
|
+
|
|
38
|
+
if path = Config.find(start)
|
|
39
|
+
Config.parse(path)
|
|
40
|
+
else
|
|
41
|
+
Config.new(nil, start, DEFAULT_SCRIPTS_DIR)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns the path to the first .sem file found walking up from
|
|
46
|
+
# dir to the filesystem root, or nil if there is none.
|
|
47
|
+
def Config.find(dir)
|
|
48
|
+
current = File.expand_path(dir)
|
|
49
|
+
loop do
|
|
50
|
+
candidate = File.join(current, FILENAME)
|
|
51
|
+
if File.file?(candidate)
|
|
52
|
+
return candidate
|
|
53
|
+
end
|
|
54
|
+
parent = File.dirname(current)
|
|
55
|
+
if parent == current
|
|
56
|
+
return nil
|
|
57
|
+
end
|
|
58
|
+
current = parent
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def Config.parse(path)
|
|
63
|
+
Preconditions.check_state(File.file?(path), "File[%s] does not exist" % path)
|
|
64
|
+
|
|
65
|
+
values = {}
|
|
66
|
+
File.foreach(path).each_with_index do |line, index|
|
|
67
|
+
stripped = line.strip
|
|
68
|
+
if stripped.empty? || stripped.start_with?("#")
|
|
69
|
+
next
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
location = "File[%s] line %s" % [path, index + 1]
|
|
73
|
+
name, value = Library.parse_property(stripped, PROPERTY_PREFIX)
|
|
74
|
+
Preconditions.check_state(!name.nil? && !value.nil?,
|
|
75
|
+
"%s: Invalid line[%s]. Expected sem.config.<name> = <value>" % [location, stripped])
|
|
76
|
+
Preconditions.check_state(NAMES.include?(name),
|
|
77
|
+
"%s: Config with name[%s] not found. Must be one of: %s" % [location, name, NAMES.join(" ")])
|
|
78
|
+
Preconditions.check_state(!value.empty?,
|
|
79
|
+
"%s: Config[%s] - value cannot be blank" % [location, name])
|
|
80
|
+
Preconditions.check_state(!values.has_key?(name),
|
|
81
|
+
"%s: Config[%s] is specified more than once" % [location, name])
|
|
82
|
+
values[name] = value
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
Config.new(path, File.dirname(path), values["scripts_dir"] || DEFAULT_SCRIPTS_DIR)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
end
|
|
@@ -3,8 +3,8 @@ module SchemaEvolutionManager
|
|
|
3
3
|
class ConnectionData
|
|
4
4
|
|
|
5
5
|
DEFAULT_PORT = 5432 unless defined?(DEFAULT_PORT)
|
|
6
|
-
|
|
7
|
-
attr_reader :host, :name, :port, :user
|
|
6
|
+
|
|
7
|
+
attr_reader :host, :name, :port, :user, :password
|
|
8
8
|
|
|
9
9
|
def initialize(host, name, opts={})
|
|
10
10
|
@host = host
|
|
@@ -19,50 +19,103 @@ module SchemaEvolutionManager
|
|
|
19
19
|
Preconditions.check_argument(@port > 0, "Port must be > 0")
|
|
20
20
|
|
|
21
21
|
@user = opts.delete(:user)
|
|
22
|
+
@password = opts.delete(:password)
|
|
22
23
|
Preconditions.assert_empty_opts(opts)
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
# Returns a valid pgpass line entry representing this connection.
|
|
27
|
+
# The name parsed from a url can carry query parameters
|
|
28
|
+
# (?sslmode=require), which libpq does not match on, so they are dropped.
|
|
26
29
|
#
|
|
27
30
|
# @param password: Optional password to include in the connection string
|
|
28
31
|
def pgpass(password=nil)
|
|
29
|
-
|
|
32
|
+
database = @name.to_s.split("?", 2).first
|
|
33
|
+
[@host, @port, database, @user, password].map { |v| ConnectionData.pgpass_escape(v) }.join(":")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# libpq reads ':' as the pgpass field separator and '\' as its escape,
|
|
37
|
+
# so both must be backslash-escaped within a field.
|
|
38
|
+
def ConnectionData.pgpass_escape(value)
|
|
39
|
+
value.to_s.gsub(/[\\:]/) { |c| "\\#{c}" }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns the url with any password removed from it. The username is
|
|
43
|
+
# kept so that libpq still selects the matching pgpass entry.
|
|
44
|
+
#
|
|
45
|
+
# @param url e.g. postgres://user:pass@db.com:5553/test_db
|
|
46
|
+
def ConnectionData.strip_password(url)
|
|
47
|
+
ConnectionData.rebuild_lead(url) { |user, _password| user }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns the url with any password replaced by [REDACTED], for display.
|
|
51
|
+
def ConnectionData.redact(url)
|
|
52
|
+
ConnectionData.rebuild_lead(url) { |user, password| password.nil? ? user : "#{user}:[REDACTED]" }
|
|
30
53
|
end
|
|
31
54
|
|
|
32
55
|
# Parses a connection string into a ConnectionData instance. You
|
|
33
|
-
# will get an error if the URL could not be parsed.
|
|
56
|
+
# will get an error if the URL could not be parsed. A password in the
|
|
57
|
+
# url is percent-decoded, as libpq does, and never appears in an error.
|
|
34
58
|
#
|
|
35
59
|
# @param url e.g. postgres://user1@db.com:5553/test_db
|
|
36
60
|
def ConnectionData.parse_url(url)
|
|
61
|
+
display = ConnectionData.redact(url)
|
|
37
62
|
protocol, rest = url.split("//", 2)
|
|
38
63
|
if rest.nil?
|
|
39
|
-
raise "Invalid url[%s]. Expected to start with postgres://" %
|
|
64
|
+
raise "Invalid url[%s]. Expected to start with postgres://" % display
|
|
40
65
|
end
|
|
41
|
-
|
|
66
|
+
|
|
42
67
|
lead, name = rest.split("/", 2)
|
|
43
68
|
if name.nil?
|
|
44
|
-
raise "Invalid url[%s]. Missing database name" %
|
|
69
|
+
raise "Invalid url[%s]. Missing database name" % display
|
|
45
70
|
end
|
|
46
71
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
user = parts[0]
|
|
50
|
-
db_host = parts[1]
|
|
51
|
-
else
|
|
52
|
-
user = nil
|
|
53
|
-
db_host = lead
|
|
54
|
-
end
|
|
72
|
+
userinfo, db_host = ConnectionData.split_lead(lead)
|
|
73
|
+
user, password = ConnectionData.split_userinfo(userinfo)
|
|
55
74
|
|
|
56
75
|
host, port = db_host.split(":", 2)
|
|
57
76
|
if port
|
|
58
77
|
if port.to_i.to_s != port
|
|
59
|
-
raise "Invalid url[%s]. Expected database port[%s] to be an integer" % [
|
|
78
|
+
raise "Invalid url[%s]. Expected database port[%s] to be an integer" % [display, port]
|
|
60
79
|
end
|
|
61
80
|
end
|
|
62
81
|
|
|
63
|
-
ConnectionData.new(host, name, :user => user
|
|
82
|
+
ConnectionData.new(host, name, :user => user && ConnectionData.percent_decode(user),
|
|
83
|
+
:password => password && ConnectionData.percent_decode(password), :port => port)
|
|
64
84
|
end
|
|
65
|
-
|
|
85
|
+
|
|
86
|
+
# Splits "userinfo@host:port" on the LAST '@', since an unencoded
|
|
87
|
+
# password may itself contain one. Returns [userinfo or nil, host:port]
|
|
88
|
+
def ConnectionData.split_lead(lead)
|
|
89
|
+
index = lead.rindex("@")
|
|
90
|
+
index.nil? ? [nil, lead] : [lead[0...index], lead[(index + 1)..-1]]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Splits "user:password" on the FIRST ':'. Returns [user, password or nil]
|
|
94
|
+
def ConnectionData.split_userinfo(userinfo)
|
|
95
|
+
return [nil, nil] if userinfo.nil?
|
|
96
|
+
user, password = userinfo.split(":", 2)
|
|
97
|
+
[user, password]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def ConnectionData.percent_decode(value)
|
|
101
|
+
value.gsub(/%([0-9A-Fa-f]{2})/) { $1.hex.chr }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Yields [user, password] from the url's userinfo and returns the url
|
|
105
|
+
# with the userinfo replaced by the block's result. A url with no
|
|
106
|
+
# userinfo is returned unchanged.
|
|
107
|
+
def ConnectionData.rebuild_lead(url)
|
|
108
|
+
protocol, rest = url.split("//", 2)
|
|
109
|
+
return url if rest.nil?
|
|
110
|
+
lead, name = rest.split("/", 2)
|
|
111
|
+
userinfo, db_host = ConnectionData.split_lead(lead)
|
|
112
|
+
return url if userinfo.nil?
|
|
113
|
+
user, password = ConnectionData.split_userinfo(userinfo)
|
|
114
|
+
new_userinfo = yield(user, password)
|
|
115
|
+
new_lead = new_userinfo.to_s.empty? ? db_host : "#{new_userinfo}@#{db_host}"
|
|
116
|
+
name.nil? ? "#{protocol}//#{new_lead}" : "#{protocol}//#{new_lead}/#{name}"
|
|
117
|
+
end
|
|
118
|
+
|
|
66
119
|
end
|
|
67
120
|
|
|
68
121
|
end
|