pg_migrate 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ [submodule "lib/pg_migrate/templates"]
2
+ path = lib/pg_migrate/templates
3
+ url = git@github.com:sethcall/pg_migrate_templates.git
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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/bin/pg_migrate ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pg_migrate'
4
+
5
+ include PgMigrate
6
+
7
+ CommandLine.start
@@ -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,12 @@
1
+ module PgMigrate
2
+ class Migration
3
+ attr_accessor :name, :ordinal, :md5, :created, :production, :filepath
4
+
5
+ def initialize(name, ordinal, filepath)
6
+ @name = name
7
+ @ordinal = ordinal
8
+ @filepath = filepath
9
+ end
10
+
11
+ end
12
+ 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
+