schema-evolution-manager 0.9.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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