pg_migrate 0.0.1
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.
- data/.gitignore +34 -0
- data/.gitmodules +3 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +38 -0
- data/LICENSE +22 -0
- data/README.md +35 -0
- data/Rakefile +2 -0
- data/bin/pg_migrate +7 -0
- data/lib/pg_migrate/builder.rb +158 -0
- data/lib/pg_migrate/command_line.rb +78 -0
- data/lib/pg_migrate/config_parser.rb +49 -0
- data/lib/pg_migrate/manifest_reader.rb +103 -0
- data/lib/pg_migrate/migration.rb +12 -0
- data/lib/pg_migrate/migrator.rb +101 -0
- data/lib/pg_migrate/sql_reader.rb +51 -0
- data/lib/pg_migrate/templates/bootstrap.erb +145 -0
- data/lib/pg_migrate/templates/up.erb +28 -0
- data/lib/pg_migrate/version.rb +3 -0
- data/lib/pg_migrate.rb +35 -0
- data/pg_migrate.gemspec +28 -0
- data/spec/database.yml +9 -0
- data/spec/pg_migrate/builder_spec.rb +112 -0
- data/spec/pg_migrate/config_parser_spec.rb +19 -0
- data/spec/pg_migrate/db_utility.rb +73 -0
- data/spec/pg_migrate/input_manifests/single_manifest/manifest +4 -0
- data/spec/pg_migrate/input_manifests/single_manifest/up/single1.sql +29 -0
- data/spec/pg_migrate/manifest_reader_spec.rb +20 -0
- data/spec/pg_migrate/migrator_spec.rb +68 -0
- data/spec/pg_migrate/sql_reader_spec.rb +22 -0
- data/spec/spec_helper.rb +15 -0
- metadata +136 -0
data/.gitignore
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
coverage
|
6
|
+
InstalledFiles
|
7
|
+
lib/bundler/man
|
8
|
+
pkg
|
9
|
+
rdoc
|
10
|
+
spec/reports
|
11
|
+
test/tmp
|
12
|
+
test/version_tmp
|
13
|
+
tmp
|
14
|
+
|
15
|
+
# YARD artifacts
|
16
|
+
.yardoc
|
17
|
+
_yardoc
|
18
|
+
doc/
|
19
|
+
|
20
|
+
# Intellij artifacts
|
21
|
+
.idea
|
22
|
+
*.iml
|
23
|
+
|
24
|
+
# RVM
|
25
|
+
.rvmrc
|
26
|
+
|
27
|
+
# VIM
|
28
|
+
*~
|
29
|
+
|
30
|
+
# rspec output directory
|
31
|
+
target
|
32
|
+
|
33
|
+
# local ruby symbolic link to fool IDE
|
34
|
+
ruby
|
data/.gitmodules
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in pg_migrate.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
group :development do
|
7
|
+
gem 'rspec', '2.11.0'
|
8
|
+
#gem 'files', :git => 'git@github.com:sethcall/files.git'
|
9
|
+
gem 'files', :path => '/Users/seth/workspace/files'
|
10
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
pg_migrate (0.0.1)
|
5
|
+
logging (= 1.7.2)
|
6
|
+
pg (= 0.14.0)
|
7
|
+
thor (= 0.15.4)
|
8
|
+
|
9
|
+
PATH
|
10
|
+
remote: /Users/seth/workspace/files
|
11
|
+
specs:
|
12
|
+
files (0.2.1)
|
13
|
+
|
14
|
+
GEM
|
15
|
+
remote: https://rubygems.org/
|
16
|
+
specs:
|
17
|
+
diff-lcs (1.1.3)
|
18
|
+
little-plugger (1.1.3)
|
19
|
+
logging (1.7.2)
|
20
|
+
little-plugger (>= 1.1.3)
|
21
|
+
pg (0.14.0)
|
22
|
+
rspec (2.11.0)
|
23
|
+
rspec-core (~> 2.11.0)
|
24
|
+
rspec-expectations (~> 2.11.0)
|
25
|
+
rspec-mocks (~> 2.11.0)
|
26
|
+
rspec-core (2.11.0)
|
27
|
+
rspec-expectations (2.11.1)
|
28
|
+
diff-lcs (~> 1.1.3)
|
29
|
+
rspec-mocks (2.11.0)
|
30
|
+
thor (0.15.4)
|
31
|
+
|
32
|
+
PLATFORMS
|
33
|
+
ruby
|
34
|
+
|
35
|
+
DEPENDENCIES
|
36
|
+
files!
|
37
|
+
pg_migrate!
|
38
|
+
rspec (= 2.11.0)
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 TODO: Write your name
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# PgMigrate
|
2
|
+
|
3
|
+
The ruby implementation of pg_migrate.
|
4
|
+
|
5
|
+
With this gem, there are two primary features:
|
6
|
+
* Manifest Builder - process a user's pg_migrate manifest into a stable set of SQL migration scripts
|
7
|
+
* Migrator - migrate a database, using a built pg_migrate manifest
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
**NOTE: this gem is not yet published**
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
gem 'pg_migrate'
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install pg_migrate
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
TODO: Write usage instructions here
|
28
|
+
|
29
|
+
## Contributing
|
30
|
+
|
31
|
+
1. Fork it
|
32
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
33
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
34
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
35
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/bin/pg_migrate
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'find'
|
4
|
+
require 'erb'
|
5
|
+
|
6
|
+
|
7
|
+
module PgMigrate
|
8
|
+
# takes a unprocessed manifest directory, and adds before/after headers to each file
|
9
|
+
class Builder
|
10
|
+
|
11
|
+
|
12
|
+
attr_accessor :manifest_reader, :sql_reader
|
13
|
+
|
14
|
+
|
15
|
+
def initialize(manifest_reader, sql_reader)
|
16
|
+
@log = Logging.logger[self]
|
17
|
+
@manifest_reader = manifest_reader
|
18
|
+
@sql_reader = sql_reader
|
19
|
+
@template_dir = File.join(File.dirname(__FILE__), 'templates')
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
# input_dir is root path, contains file 'manifest' and 'migrations'
|
24
|
+
# output_dir will have a manifest and migrations folder, but processed
|
25
|
+
# force will create the output dir if needed, and *delete an existing directory* if it's in the way
|
26
|
+
def build(input_dir, output_dir, options={:force=>true})
|
27
|
+
input_dir = File.expand_path(input_dir)
|
28
|
+
output_dir = File.expand_path(output_dir)
|
29
|
+
|
30
|
+
if input_dir == output_dir
|
31
|
+
raise 'input_dir can not be same as output_dir: #{input_dir}'
|
32
|
+
end
|
33
|
+
|
34
|
+
@log.debug "building migration directory #{input_dir} and placing result at: #{output_dir}"
|
35
|
+
|
36
|
+
output = Pathname.new(output_dir)
|
37
|
+
if !output.exist?
|
38
|
+
if !options[:force]
|
39
|
+
raise "Output directory '#{output_dir}' does not exist. Create it or specify create_output_dir=true"
|
40
|
+
else
|
41
|
+
output.mkpath
|
42
|
+
end
|
43
|
+
else
|
44
|
+
# verify that it's is a directory
|
45
|
+
if !output.directory?
|
46
|
+
raise "output_dir #{output_dir} is a file; not a directory."
|
47
|
+
else
|
48
|
+
@log.debug("deleting & recreating existing output_dir #{output_dir}")
|
49
|
+
output.rmtree
|
50
|
+
output.mkpath
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# manifest always goes over mostly as-is,
|
55
|
+
# just with a comment added at top indicating our version
|
56
|
+
|
57
|
+
input_manifest = File.join(input_dir, MANIFEST_FILENAME)
|
58
|
+
output_manifest = File.join(output_dir, MANIFEST_FILENAME)
|
59
|
+
|
60
|
+
File.open(output_manifest, 'w') do |fout|
|
61
|
+
fout.puts "#{BUILDER_VERSION_HEADER}pg_migrate_ruby-#{PgMigrate::VERSION}"
|
62
|
+
IO.readlines(input_manifest).each do |input|
|
63
|
+
fout.puts input
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# in order array of manifest declarations
|
68
|
+
loaded_manifest = @manifest_reader.load_input_manifest(input_dir)
|
69
|
+
# hashed on migration name hash of manifest
|
70
|
+
|
71
|
+
loaded_manifest_hash = @manifest_reader.hash_loaded_manifest(loaded_manifest)
|
72
|
+
@manifest_reader.validate_migration_paths(input_dir, loaded_manifest)
|
73
|
+
|
74
|
+
build_up(input_dir, output_dir, loaded_manifest_hash)
|
75
|
+
end
|
76
|
+
|
77
|
+
def build_up(input_dir, output_dir, loaded_manifest_hash)
|
78
|
+
migrations_input = File.join(input_dir, UP_DIRNAME)
|
79
|
+
migrations_output = File.join(output_dir, UP_DIRNAME)
|
80
|
+
|
81
|
+
# iterate through files in input migrations path, wrapping files with transactions and other required bits
|
82
|
+
|
83
|
+
Find.find(migrations_input) do |path|
|
84
|
+
if path == ".."
|
85
|
+
Find.prune
|
86
|
+
else
|
87
|
+
@log.debug "building #{path}"
|
88
|
+
|
89
|
+
# create relative bit
|
90
|
+
relative_path = path[migrations_input.length..-1]
|
91
|
+
|
92
|
+
# create the filename correct for the input directory, for this file
|
93
|
+
migration_in_path = path
|
94
|
+
|
95
|
+
# create the filename correct for the output directory, for this file
|
96
|
+
migration_out_path = File.join(migrations_output, relative_path)
|
97
|
+
|
98
|
+
process_and_copy_up(migration_in_path, migration_out_path, relative_path, loaded_manifest_hash)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
create_bootstrap_script(migrations_output)
|
103
|
+
end
|
104
|
+
|
105
|
+
# creates the 'pg_migrations table'
|
106
|
+
def create_bootstrap_script(migration_out_path)
|
107
|
+
run_template("bootstrap.erb", binding, File.join(migration_out_path, BOOTSTRAP_FILENAME))
|
108
|
+
end
|
109
|
+
|
110
|
+
def create_wrapped_up_migration(migration_in_filepath, migration_out_filepath, migration_def)
|
111
|
+
builder_version="pg_migrate_ruby-#{PgMigrate::VERSION}"
|
112
|
+
migration_content = nil
|
113
|
+
File.open(migration_in_filepath, 'r') {|reader| migration_content = reader.read }
|
114
|
+
run_template("up.erb", binding, File.join(migration_out_filepath))
|
115
|
+
end
|
116
|
+
|
117
|
+
# given an input template and binding, writes to an output file
|
118
|
+
def run_template(template, binding, output_filepath)
|
119
|
+
bootstrap_template = nil
|
120
|
+
File.open(File.join(@template_dir, template), 'r') do |reader|
|
121
|
+
bootstrap_template = reader.read
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
template = ERB.new(bootstrap_template, 0, "%<>")
|
126
|
+
content = template.result(binding)
|
127
|
+
File.open(output_filepath, 'w') do |writer|
|
128
|
+
writer.syswrite(content)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def process_and_copy_up(migration_in_path, migration_out_path, relative_path, loaded_manifest_hash)
|
133
|
+
|
134
|
+
if FileTest.directory?(migration_in_path)
|
135
|
+
# copy over directories
|
136
|
+
# find relative-to-migrations dir this path
|
137
|
+
FileUtils.mkdir(migration_out_path)
|
138
|
+
else
|
139
|
+
if migration_in_path.end_with?('.sql')
|
140
|
+
# if a .sql file, then copy & process
|
141
|
+
|
142
|
+
# create the the 'key' version of this name, which is basically the filepath
|
143
|
+
# of the .sql file relative without the leading '/' directory
|
144
|
+
manifest_name = relative_path[1..-1]
|
145
|
+
|
146
|
+
@log.debug("retrieving manifest definition for #{manifest_name}")
|
147
|
+
|
148
|
+
migration_def = loaded_manifest_hash[manifest_name]
|
149
|
+
|
150
|
+
create_wrapped_up_migration(migration_in_path, migration_out_path, migration_def)
|
151
|
+
else
|
152
|
+
# if not a .sql file, just copy it over
|
153
|
+
FileUtils.cp(migration_in_path, migration_out_path)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module PgMigrate
|
2
|
+
class CommandLine < Thor
|
3
|
+
|
4
|
+
desc "up", "migrates the database forwards, applying migrations found in the source directory"
|
5
|
+
method_option :source, :aliases => "-s", :default => '.', :lazy_default => '.', :banner => 'input directory', :desc => "a pg_migrate built manifest. Should contain your processed manifest and up|down|test folders"
|
6
|
+
method_option :connopts, :aliases => "-o", :type => :hash, :required => true, :banner => "connection options", :desc => "database connection options used by gem 'pg': dbname|host|hostaddr|port|user|password|connection_timeout|options|sslmode|krbsrvname|gsslib|service"
|
7
|
+
method_option :verbose, :aliases => "-v", :type => :boolean, :default => false, :banner => "verbose", :desc=> "set to raise verbosity"
|
8
|
+
|
9
|
+
def up
|
10
|
+
bootstrap_logger(options[:verbose])
|
11
|
+
|
12
|
+
manifest_reader = ManifestReader.new
|
13
|
+
sql_reader = SqlReader.new
|
14
|
+
|
15
|
+
connopts = options[:connopts]
|
16
|
+
if !connopts[:port].nil?
|
17
|
+
connopts[:port] = connopts[:port].to_i
|
18
|
+
end
|
19
|
+
|
20
|
+
migrator = Migrator.new(manifest_reader, sql_reader, connopts)
|
21
|
+
|
22
|
+
begin
|
23
|
+
migrator.migrate(options[:source])
|
24
|
+
rescue Exception => e
|
25
|
+
if !options[:verbose]
|
26
|
+
# catch common exceptions and make pretty on command-line
|
27
|
+
if !e.message.index("ManifestReader: code=unloadable_manifest").nil?
|
28
|
+
puts "Unable to load manifest in source directory '#{options[:source]}' . Check -s|--source option and run again."
|
29
|
+
exit 1
|
30
|
+
else
|
31
|
+
raise e
|
32
|
+
end
|
33
|
+
else
|
34
|
+
raise e
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
desc "down", "not implemented"
|
42
|
+
|
43
|
+
def down
|
44
|
+
bootstrap_logger(options[:verbose])
|
45
|
+
|
46
|
+
raise 'Not implemented'
|
47
|
+
end
|
48
|
+
|
49
|
+
desc "build", "processes a pg_migrate source directory and places the result in the specified output directory"
|
50
|
+
method_option :source, :aliases => "-s", :default => '.', :lazy_default => '.', :banner => 'input directory', :desc => "the input directory containing a manifest file and up|down|test folders"
|
51
|
+
method_option :out, :aliases => "-o", :required => true, :banner => "output directory", :desc => "where the processed migrations will be placed"
|
52
|
+
method_option :force, :aliases => "-f", :default => false, :type => :boolean, :banner => "overwrite out", :desc => "if specified, the out directory will be created before processing occurs, replacing any existing directory"
|
53
|
+
method_option :verbose, :aliases => "-v", :type => :boolean, :default => false, :banner => "verbose", :desc=> "set to raise verbosity"
|
54
|
+
|
55
|
+
def build
|
56
|
+
|
57
|
+
bootstrap_logger(options[:verbose])
|
58
|
+
|
59
|
+
manifest_reader = ManifestReader.new
|
60
|
+
sql_reader = SqlReader.new
|
61
|
+
builder = Builder.new(manifest_reader, sql_reader)
|
62
|
+
builder.build(options[:source], options[:out], :force => options[:force])
|
63
|
+
end
|
64
|
+
|
65
|
+
no_tasks do
|
66
|
+
def bootstrap_logger(verbose)
|
67
|
+
# bootstrap logger
|
68
|
+
if verbose
|
69
|
+
Logging.logger.root.level = :debug
|
70
|
+
else
|
71
|
+
Logging.logger.root.level = :info
|
72
|
+
end
|
73
|
+
|
74
|
+
Logging.logger.root.appenders = Logging.appenders.stdout
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module PgMigrate
|
4
|
+
|
5
|
+
class ConfigParser
|
6
|
+
|
7
|
+
def self.rails(path, environment)
|
8
|
+
|
9
|
+
config = {}
|
10
|
+
|
11
|
+
rails_config = YAML.load_file(path)
|
12
|
+
|
13
|
+
if !rails_config.has_key?(environment)
|
14
|
+
raise "no environment #{environment} found in rails config file: #{path}"
|
15
|
+
end
|
16
|
+
|
17
|
+
rails_config = rails_config[environment]
|
18
|
+
|
19
|
+
# populate from rails YAML to PG
|
20
|
+
|
21
|
+
# required parameters 1st
|
22
|
+
if !rails_config.has_key?("database")
|
23
|
+
raise "no database key found in #{path} with environment #{environment}"
|
24
|
+
end
|
25
|
+
|
26
|
+
config[:dbname] = rails_config["database"]
|
27
|
+
|
28
|
+
if rails_config.has_key?("host")
|
29
|
+
config[:host] = rails_config["host"]
|
30
|
+
end
|
31
|
+
|
32
|
+
if rails_config.has_key?("port")
|
33
|
+
config[:port] = rails_config["port"]
|
34
|
+
end
|
35
|
+
|
36
|
+
if rails_config.has_key?("username")
|
37
|
+
config[:user] = rails_config["username"]
|
38
|
+
end
|
39
|
+
|
40
|
+
if rails_config.has_key?("password")
|
41
|
+
config[:password] = rails_config["password"]
|
42
|
+
end
|
43
|
+
|
44
|
+
return config
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module PgMigrate
|
4
|
+
class ManifestReader
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@log = Logging.logger[self]
|
8
|
+
end
|
9
|
+
|
10
|
+
# returns array of migration paths
|
11
|
+
def load_input_manifest(manifest_path)
|
12
|
+
manifest, version = load_manifest(manifest_path, false)
|
13
|
+
return manifest
|
14
|
+
end
|
15
|
+
|
16
|
+
# returns [array of migration paths, version]
|
17
|
+
def load_output_manifest(manifest_path)
|
18
|
+
return load_manifest(manifest_path, true)
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
# verify that the migration files exist
|
23
|
+
def validate_migration_paths(manifest_path, manifest)
|
24
|
+
# each item in the manifest should be a valid file
|
25
|
+
manifest.each do |item|
|
26
|
+
item_path = build_migration_path(manifest_path, item.name)
|
27
|
+
if !Pathname.new(item_path).exist?
|
28
|
+
raise "manifest reference #{item.name} does not exist at path #{item_path}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# construct a migration file path location based on the manifest basedir and the name of the migration
|
34
|
+
def build_migration_path(manifest_path, migration_name)
|
35
|
+
File.join(manifest_path, UP_DIRNAME, "#{migration_name}")
|
36
|
+
end
|
37
|
+
|
38
|
+
def hash_loaded_manifest(loaded_manifest)
|
39
|
+
hash = {}
|
40
|
+
loaded_manifest.each do |manifest|
|
41
|
+
hash[manifest.name] = manifest
|
42
|
+
end
|
43
|
+
return hash
|
44
|
+
end
|
45
|
+
|
46
|
+
# read in the manifest, saving each migration declaration in order as they are found
|
47
|
+
private
|
48
|
+
def load_manifest(manifest_path, is_output)
|
49
|
+
|
50
|
+
manifest = []
|
51
|
+
version = nil
|
52
|
+
|
53
|
+
manifest_filepath = File.join(manifest_path, MANIFEST_FILENAME)
|
54
|
+
|
55
|
+
@log.debug "loading manifest from #{manifest_path}"
|
56
|
+
|
57
|
+
if !FileTest::exist?(manifest_filepath)
|
58
|
+
raise "ManifestReader: code=unloadable_manifest: manifest not found at #{manifest_path}"
|
59
|
+
end
|
60
|
+
|
61
|
+
# there should be a file called 'manifest' at this location
|
62
|
+
manifest_lines = IO.readlines(manifest_filepath)
|
63
|
+
|
64
|
+
ordinal = 0
|
65
|
+
manifest_lines.each_with_index do |line, index|
|
66
|
+
# ignore comments
|
67
|
+
migration_name = line.strip
|
68
|
+
|
69
|
+
@log.debug "processing line:#{index} #{line}"
|
70
|
+
|
71
|
+
# output files must have a version header as 1st line o file
|
72
|
+
if is_output
|
73
|
+
if index == 0
|
74
|
+
# the first line must be the version comment. if not, error out.
|
75
|
+
if migration_name.index(BUILDER_VERSION_HEADER) == 0 && migration_name.length > BUILDER_VERSION_HEADER.length
|
76
|
+
version = migration_name[BUILDER_VERSION_HEADER.length..-1]
|
77
|
+
@log.debug "manifest has builder_version #{version}"
|
78
|
+
else
|
79
|
+
raise "manifest invalid: missing/malformed version. expecting '# pg_migrate-VERSION' to begin first line '#{line}' of manifest file: '#{manifest_path}'"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
if migration_name.empty? or migration_name.start_with?('#')
|
85
|
+
# ignored!
|
86
|
+
else
|
87
|
+
@log.debug "adding manifest #{migration_name} with ordinal #{ordinal}"
|
88
|
+
manifest.push(Migration.new(migration_name, ordinal, build_migration_path(manifest_path, migration_name)))
|
89
|
+
ordinal += 1
|
90
|
+
end
|
91
|
+
|
92
|
+
# the logic above wouldn't get upset with an empty manifest
|
93
|
+
if is_output
|
94
|
+
if version.nil?
|
95
|
+
raise "manifest invalid: empty"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
return manifest, version
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module PgMigrate
|
2
|
+
|
3
|
+
class Migrator
|
4
|
+
|
5
|
+
attr_accessor :conn, :connection_hash, :manifest_path, :manifest, :manifest_reader, :sql_reader
|
6
|
+
|
7
|
+
# options = gem 'pg' connection_hash options, or connstring => dbname=test port=5432, or pgconn => PG::Connection object
|
8
|
+
def initialize(manifest_reader, sql_reader, options = {})
|
9
|
+
@log = Logging.logger[self]
|
10
|
+
@connection_hash = options
|
11
|
+
@manifest = nil
|
12
|
+
@builder_version = nil
|
13
|
+
@manifest_reader = manifest_reader
|
14
|
+
@sql_reader = sql_reader
|
15
|
+
end
|
16
|
+
|
17
|
+
# 'migrate' attempt to migrate your database based on the contents of your built manifest
|
18
|
+
# The manifest_path argument should point to your manifest
|
19
|
+
# manifest_path = the directory containing your 'manifest' file, 'up' directory, 'down' directory, 'test' directory
|
20
|
+
# this method will throw an exception if anything goes wrong (such as bad SQL in the migrations themselves)
|
21
|
+
|
22
|
+
def migrate(manifest_path)
|
23
|
+
@manifest_path = manifest_path
|
24
|
+
|
25
|
+
if !@connection_hash[:pgconn].nil?
|
26
|
+
@conn = @connection_hash[:pgconn]
|
27
|
+
elsif !@connection_hash[:connstring].nil?
|
28
|
+
@conn = PG::Connection.open(@connection_hash[:connstring])
|
29
|
+
else
|
30
|
+
@conn = PG::Connection.open(@connection_hash)
|
31
|
+
end
|
32
|
+
|
33
|
+
# this is used to record the version of the 'migrator' in the pg_migrate table
|
34
|
+
@conn.exec("SET application_name = 'pg_migrate_ruby-#{PgMigrate::VERSION}'")
|
35
|
+
|
36
|
+
# load the manifest, and version of the builder that made it
|
37
|
+
process_manifest()
|
38
|
+
|
39
|
+
# execute the migrations
|
40
|
+
run_migrations()
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# load the manifest's migration declarations, and validate that each migration points to a real file
|
45
|
+
def process_manifest
|
46
|
+
@manifest, @builder_version = @manifest_reader.load_output_manifest(@manifest_path)
|
47
|
+
@manifest_reader.validate_migration_paths(@manifest_path, @manifest)
|
48
|
+
end
|
49
|
+
|
50
|
+
# run all necessary migrations
|
51
|
+
def run_migrations
|
52
|
+
|
53
|
+
# run bootstrap before user migrations to prepare database
|
54
|
+
run_bootstrap
|
55
|
+
|
56
|
+
# loop through the manifest, executing migrations in turn
|
57
|
+
manifest.each_with_index do |migration, index|
|
58
|
+
execute_migration(migration.name, migration.filepath)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
# executes the bootstrap method
|
64
|
+
def run_bootstrap
|
65
|
+
bootstrap = File.join(@manifest_path, UP_DIRNAME, BOOTSTRAP_FILENAME)
|
66
|
+
execute_migration('bootstrap.sql', bootstrap)
|
67
|
+
end
|
68
|
+
|
69
|
+
# execute a single migration by loading it's statements from file, and then executing each
|
70
|
+
def execute_migration(name, filepath)
|
71
|
+
@log.debug "executing migration #{filepath}"
|
72
|
+
|
73
|
+
statements = @sql_reader.load_migration(filepath)
|
74
|
+
if statements.length == 0
|
75
|
+
raise 'no statements found in migration #{migration_path}'
|
76
|
+
end
|
77
|
+
run_migration(name, statements)
|
78
|
+
end
|
79
|
+
|
80
|
+
# execute all the statements of a single migration
|
81
|
+
def run_migration(name, statements)
|
82
|
+
|
83
|
+
begin
|
84
|
+
statements.each do |statement|
|
85
|
+
conn.exec(statement).clear
|
86
|
+
end
|
87
|
+
rescue Exception => e
|
88
|
+
# we make a special allowance for one exception; it just means this migration
|
89
|
+
# has already occured, and we should just treat it like a continue
|
90
|
+
if e.message.index('pg_migrate: code=migration_exists').nil?
|
91
|
+
conn.exec("ROLLBACK")
|
92
|
+
raise e
|
93
|
+
else
|
94
|
+
conn.exec("ROLLBACK")
|
95
|
+
@log.info "migration #{name} already run"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|