schema-evolution-manager 0.9.24

Sign up to get free protection for your applications and to get access to all the features.
@@ -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