pg_migrate 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,51 @@
|
|
1
|
+
module PgMigrate
|
2
|
+
|
3
|
+
class SqlReader
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
|
7
|
+
end
|
8
|
+
|
9
|
+
|
10
|
+
# read in a migration file,
|
11
|
+
# converting lines of text into SQL statements that can be executed with our database connection
|
12
|
+
def load_migration(migration_path)
|
13
|
+
statements = []
|
14
|
+
|
15
|
+
current_statement = ""
|
16
|
+
|
17
|
+
migration_lines = IO.readlines(migration_path)
|
18
|
+
migration_lines.each_with_index do |line, index|
|
19
|
+
line_stripped = line.strip
|
20
|
+
|
21
|
+
if line_stripped.empty? || line_stripped.start_with?('--')
|
22
|
+
# it's a comment; ignore
|
23
|
+
elsif line_stripped.start_with?("\\")
|
24
|
+
# it's a psql command; ignore
|
25
|
+
else
|
26
|
+
current_statement += " " + line_stripped;
|
27
|
+
|
28
|
+
if line_stripped.end_with?(";")
|
29
|
+
if current_statement =~ /^\s*CREATE\s+(OR\s+REPLACE\s+)?FUNCTION/i
|
30
|
+
# if we are in a function, a ';' isn't enough to end. We need to see if the last word was one of
|
31
|
+
# pltcl, plperl, plpgsql, plpythonu, sql.
|
32
|
+
# you can extend languages in postgresql; detecting these isn't supported yet.
|
33
|
+
|
34
|
+
if current_statement =~ /(plpgsql|plperl|plpythonu|pltcl|sql)\s*;$/i
|
35
|
+
statements.push(current_statement[0...-1]) # strip off last ;
|
36
|
+
current_statement = ""
|
37
|
+
end
|
38
|
+
|
39
|
+
else
|
40
|
+
statements.push(current_statement[0...-1]) # strip off last ;
|
41
|
+
current_statement = ""
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
return statements
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
-- pg_migrate bootstrap
|
2
|
+
-- purpose: responsible for creating or updating pg_migration internal tables
|
3
|
+
-- when: this script should be called before every migration attempt, to be safe.
|
4
|
+
-- except: if you know you have not updated the pg_migrate tool itself,
|
5
|
+
-- and you know you have run this file at least once before on the current database,
|
6
|
+
-- then you don't have to run this again.
|
7
|
+
|
8
|
+
\set ON_ERROR_STOP 1
|
9
|
+
|
10
|
+
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
11
|
+
|
12
|
+
CREATE OR REPLACE FUNCTION bootstrap_pg_migrate() RETURNS void AS $$
|
13
|
+
DECLARE
|
14
|
+
found_pg_migrate information_schema.tables;
|
15
|
+
found_pg_migrations information_schema.tables;
|
16
|
+
|
17
|
+
BEGIN
|
18
|
+
BEGIN
|
19
|
+
SELECT * INTO STRICT found_pg_migrate FROM information_schema.tables WHERE table_name = 'pg_migrate';
|
20
|
+
|
21
|
+
EXCEPTION
|
22
|
+
WHEN NO_DATA_FOUND THEN
|
23
|
+
CREATE TABLE pg_migrate (id BIGSERIAL PRIMARY KEY, template_version VARCHAR(255), builder_version VARCHAR(255), migrator_version VARCHAR(255), database_version VARCHAR(1024));
|
24
|
+
CREATE INDEX pg_migrate_unique_index ON pg_migrate (template_version, builder_version, migrator_version, database_version);
|
25
|
+
|
26
|
+
WHEN TOO_MANY_ROWS THEN
|
27
|
+
RAISE EXCEPTION 'Multiple pg_migrate tables. Unique key on information_schema.tables should have prevented this.';
|
28
|
+
END;
|
29
|
+
|
30
|
+
BEGIN
|
31
|
+
SELECT * INTO STRICT found_pg_migrations FROM information_schema.tables WHERE table_name = 'pg_migrations';
|
32
|
+
|
33
|
+
EXCEPTION
|
34
|
+
WHEN NO_DATA_FOUND THEN
|
35
|
+
CREATE TABLE pg_migrations(
|
36
|
+
name VARCHAR(255) PRIMARY KEY,
|
37
|
+
ordinal INTEGER NOT NULL,
|
38
|
+
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
39
|
+
finalized SMALLINT DEFAULT 1,
|
40
|
+
pg_migrate_id BIGINT NOT NULL REFERENCES pg_migrate(id));
|
41
|
+
WHEN TOO_MANY_ROWS THEN
|
42
|
+
RAISE EXCEPTION 'Multiple pg_migrations tables. Unique key on information_schema.tables should have prevented this.';
|
43
|
+
END;
|
44
|
+
|
45
|
+
END;
|
46
|
+
$$ LANGUAGE plpgsql;
|
47
|
+
|
48
|
+
|
49
|
+
SELECT bootstrap_pg_migrate();
|
50
|
+
|
51
|
+
-- verifies that the specified migration name has the expected ordinal. No exception is thrown if no migration
|
52
|
+
-- of this name exists, and no exception is thrown if the migration + ordinal pair is found on a single row.
|
53
|
+
-- Only if the migrtion is found with the wrong row is an exception thrown.
|
54
|
+
CREATE OR REPLACE FUNCTION verify_against_existing_migrations(migration varchar(255), ordinal integer) RETURNS VOID AS $$
|
55
|
+
DECLARE
|
56
|
+
found_migration pg_migrations;
|
57
|
+
BEGIN
|
58
|
+
BEGIN
|
59
|
+
EXECUTE 'SELECT * FROM pg_migrations WHERE name=$1' INTO STRICT found_migration USING migration;
|
60
|
+
EXCEPTION
|
61
|
+
WHEN NO_DATA_FOUND THEN
|
62
|
+
-- if no data, then this migration is unrun. One more check remains...
|
63
|
+
-- if the last run migration's ordinal is 1 less than this one, we are ready to go.
|
64
|
+
IF coalesce((SELECT MAX(p.ordinal) FROM pg_migrations as p), -1) <> ordinal - 1 THEN
|
65
|
+
RAISE EXCEPTION 'pg_migrate: code=missing_migration, migration=%, ordinal=%, last_ordinal=%', migration, ordinal, (SELECT MAX(p.ordinal) FROM pg_migrations as p);
|
66
|
+
END IF;
|
67
|
+
|
68
|
+
RETURN;
|
69
|
+
WHEN TOO_MANY_ROWS THEN
|
70
|
+
-- this is certainly an odd scenario, because how could this happen unless the user dropped unique key on 'name' column?
|
71
|
+
RAISE EXCEPTION 'pg_migrate: code=pg_migrations_uniqueness_error, migration=%', migration;
|
72
|
+
END;
|
73
|
+
|
74
|
+
-- one row has been found; verify ordinal is correct
|
75
|
+
IF found_migration.ordinal <> ordinal THEN
|
76
|
+
RAISE EXCEPTION 'pg_migrate: code=incorrect_ordinal, migration=%, expected_ordinal=%, actual_ordinal', migration, ordinal, found_migration.ordinal;
|
77
|
+
END IF;
|
78
|
+
|
79
|
+
END;
|
80
|
+
$$ LANGUAGE plpgsql;
|
81
|
+
|
82
|
+
|
83
|
+
-- checks if the current script has been executed or not, and throws an exception if so
|
84
|
+
-- callers should check for 'pg_migrate: code=migration_exists' to know whether they should quietly ignore exception.
|
85
|
+
-- psql can only do this by calling \set ON_ERROR_STOP 1 (which is done by templates already)
|
86
|
+
CREATE OR REPLACE FUNCTION bypass_existing_migration(migration varchar(255)) RETURNS VOID AS $$
|
87
|
+
DECLARE
|
88
|
+
found_migration pg_migrations;
|
89
|
+
BEGIN
|
90
|
+
BEGIN
|
91
|
+
EXECUTE 'SELECT * FROM pg_migrations WHERE name=$1' INTO STRICT found_migration USING migration ;
|
92
|
+
EXCEPTION
|
93
|
+
WHEN NO_DATA_FOUND THEN
|
94
|
+
-- if no data, then success. just return with no exception thrown
|
95
|
+
RETURN;
|
96
|
+
WHEN TOO_MANY_ROWS THEN
|
97
|
+
-- this path should never happen because this same condition is already checked via 'verify_against_existing_migrations'
|
98
|
+
RAISE EXCEPTION 'pg_migrate: code=pg_migrations_uniqueness_error, migration=%', migration;
|
99
|
+
END;
|
100
|
+
|
101
|
+
RAISE EXCEPTION 'pg_migrate: code=migration_exists, migration=%', migration;
|
102
|
+
END;
|
103
|
+
$$ LANGUAGE plpgsql;
|
104
|
+
|
105
|
+
|
106
|
+
-- used to mark a migration as finished
|
107
|
+
CREATE OR REPLACE FUNCTION record_migration(migration varchar(255), ordinal INTEGER, template_version VARCHAR(255), builder_version VARCHAR(255)) RETURNS VOID AS $$
|
108
|
+
DECLARE
|
109
|
+
found_pg_migrate_id BIGINT;
|
110
|
+
migrator_version VARCHAR(255);
|
111
|
+
BEGIN
|
112
|
+
|
113
|
+
EXECUTE 'SELECT current_setting(''application_name'')' INTO migrator_version;
|
114
|
+
|
115
|
+
BEGIN
|
116
|
+
-- first look for existing pg_migrate row satisfying version columns
|
117
|
+
-- but if not found, create that row
|
118
|
+
-- in either case, found_pg_migrate_id will have the row id
|
119
|
+
EXECUTE 'SELECT id FROM pg_migrate WHERE
|
120
|
+
template_version=$1 and builder_version=$2 and migrator_version=$3 and database_version=$4'
|
121
|
+
INTO found_pg_migrate_id USING template_version, builder_version, migrator_version, (select version());
|
122
|
+
EXCEPTION
|
123
|
+
WHEN NO_DATA_FOUND THEN
|
124
|
+
found_pg_migrate_id = NULL;
|
125
|
+
WHEN TOO_MANY_ROWS THEN
|
126
|
+
-- this path should never occur because of the multi-column index on pg_migrate
|
127
|
+
RAISE EXCEPTION 'pg_migrate: code=pg_migrate_uniqueness_error, migration=%, ordinal=%, template_version=%, builder_version=%, migrator_version=%, database_version', migration, ordinal, template_version, builder_version, migrator_version, (select version());
|
128
|
+
END;
|
129
|
+
|
130
|
+
IF found_pg_migrate_id IS NULL THEN
|
131
|
+
INSERT INTO pg_migrate(id, template_version, builder_version, migrator_version, database_version)
|
132
|
+
VALUES (default, template_version, builder_version, migrator_version, (select version())) RETURNING id INTO found_pg_migrate_id;
|
133
|
+
END IF;
|
134
|
+
|
135
|
+
-- create a new record in pg_migrations table, ensuring this migration won't be run again
|
136
|
+
EXECUTE 'INSERT INTO pg_migrations(name, ordinal, created, finalized, pg_migrate_id)
|
137
|
+
VALUES ($1, $2, CURRENT_TIMESTAMP, 1, $3)' USING migration, ordinal, found_pg_migrate_id;
|
138
|
+
|
139
|
+
END;
|
140
|
+
$$ LANGUAGE plpgsql;
|
141
|
+
|
142
|
+
COMMIT;
|
143
|
+
|
144
|
+
|
145
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
-- beginning pg_migrate header, migration=<%= migration_def.name %>
|
2
|
+
\set ON_ERROR_STOP 1
|
3
|
+
|
4
|
+
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
5
|
+
|
6
|
+
-- we want to ensure no one else is running this migration concurrently
|
7
|
+
LOCK TABLE pg_migrations IN ACCESS EXCLUSIVE MODE;
|
8
|
+
|
9
|
+
SELECT verify_against_existing_migrations('<%= migration_def.name %>', <%= migration_def.ordinal %>);
|
10
|
+
|
11
|
+
-- to allow exception in CHECK_IF_MIGRATED to cause rollback, not failure
|
12
|
+
\set ON_ERROR_STOP 0
|
13
|
+
|
14
|
+
SELECT bypass_existing_migration('<%= migration_def.name %>');
|
15
|
+
|
16
|
+
-- if the content of the migration is bad, we want psql to stop.
|
17
|
+
\set ON_ERROR_STOP 1
|
18
|
+
|
19
|
+
-- beginning user sql, migration=<%= migration_def.name %>
|
20
|
+
|
21
|
+
<%= migration_content %>
|
22
|
+
|
23
|
+
-- beginning pg_migrate footer, migration=<%= migration_def.name %>
|
24
|
+
|
25
|
+
SELECT record_migration('<%= migration_def.name %>', <%= migration_def.ordinal %>, '0.0.1', '<%= builder_version %>');
|
26
|
+
|
27
|
+
COMMIT;
|
28
|
+
-- end pg_migrate migration, migration=<%= migration_def.name %>
|
data/lib/pg_migrate.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'logging'
|
2
|
+
require 'pg'
|
3
|
+
require 'thor'
|
4
|
+
require "pg_migrate/version"
|
5
|
+
require "pg_migrate/migration"
|
6
|
+
require "pg_migrate/sql_reader"
|
7
|
+
require "pg_migrate/manifest_reader"
|
8
|
+
require "pg_migrate/migrator"
|
9
|
+
require "pg_migrate/config_parser"
|
10
|
+
require "pg_migrate/builder"
|
11
|
+
require "pg_migrate/command_line"
|
12
|
+
|
13
|
+
# name of the manifest file
|
14
|
+
MANIFEST_FILENAME = 'manifest'
|
15
|
+
# name of the 'forward' migration folder
|
16
|
+
UP_DIRNAME = 'up'
|
17
|
+
# name of the 'backwards' migration folder
|
18
|
+
DOWN_DIRNAME = 'down'
|
19
|
+
# name of the 'test' migration folder
|
20
|
+
TESTDIRNAME = 'test'
|
21
|
+
# name of the bootstrap.sql file
|
22
|
+
BOOTSTRAP_FILENAME = "bootstrap.sql"
|
23
|
+
# built manifest version header
|
24
|
+
BUILDER_VERSION_HEADER="# pg_migrate-"
|
25
|
+
|
26
|
+
|
27
|
+
### SQL CONSTANTS ###
|
28
|
+
PG_MIGRATE_TABLE = "pg_migrate"
|
29
|
+
PG_MIGRATIONS_TABLE = "pg_migrations"
|
30
|
+
|
31
|
+
|
32
|
+
|
33
|
+
module PgMigrate
|
34
|
+
# Your code goes here...
|
35
|
+
end
|
data/pg_migrate.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/pg_migrate/version', __FILE__)
|
3
|
+
lib=File.expand_path('../lib', __FILE__)
|
4
|
+
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.authors = ["Seth Call"]
|
8
|
+
gem.email = ["sethcall@gmail.com"]
|
9
|
+
gem.description = %q{Simple migration tool focused on Postgresql}
|
10
|
+
gem.summary = %q{Create migration scripts in raw SQL that work regardless if they are run from the pg_migrate command-line, psql, or native code integration. More documentation exists on the project homepage.)}
|
11
|
+
gem.homepage = "https://github.com/sethcall/pg_migrate"
|
12
|
+
|
13
|
+
gem.files = `git ls-files`.split($\)
|
14
|
+
gem.files += `cd #{lib}/pg_migrate/templates; git ls-files *.erb`.split($\).map {|f| "lib/pg_migrate/templates/#{f}"}
|
15
|
+
puts gem.files
|
16
|
+
gem.files.delete("lib/pg_migrate/templates")
|
17
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
18
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
|
+
gem.name = "pg_migrate"
|
20
|
+
gem.require_paths = ["lib"]
|
21
|
+
gem.version = PgMigrate::VERSION
|
22
|
+
|
23
|
+
gem.add_dependency('logging', '1.7.2')
|
24
|
+
|
25
|
+
gem.add_dependency('pg', '0.14.0')
|
26
|
+
gem.add_dependency('thor', '0.15.4')
|
27
|
+
end
|
28
|
+
|
data/spec/database.yml
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Builder do
|
4
|
+
|
5
|
+
before(:all) do
|
6
|
+
@manifest_reader = ManifestReader.new
|
7
|
+
@sql_reader = SqlReader.new
|
8
|
+
@standard_builder = Builder.new(@manifest_reader, @sql_reader)
|
9
|
+
@dbutil = DbUtility.new
|
10
|
+
end
|
11
|
+
|
12
|
+
it "create bootstrap.sql" do
|
13
|
+
standard_builder = @standard_builder
|
14
|
+
target = Files.create :path => "target/bootstrap_test", :timestamp => false do
|
15
|
+
standard_builder.create_bootstrap_script(Dir.pwd)
|
16
|
+
|
17
|
+
# the .sql file should exist after
|
18
|
+
FileTest::exists?(BOOTSTRAP_FILENAME).should == true
|
19
|
+
|
20
|
+
content = nil
|
21
|
+
|
22
|
+
# dynamic content should be in the file
|
23
|
+
File.open(BOOTSTRAP_FILENAME, 'r') { |reader| content = reader.read }
|
24
|
+
|
25
|
+
content.start_with?('-- pg_migrate bootstrap').should == true
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
it "creates indempotent migrations" do
|
31
|
+
|
32
|
+
def run_bootstrap(output_dir)
|
33
|
+
run_migration(BOOTSTRAP_FILENAME, output_dir)
|
34
|
+
end
|
35
|
+
|
36
|
+
def run_migration(migration_path, output_dir)
|
37
|
+
@dbutil.connect_test_database() do |conn|
|
38
|
+
statements = @sql_reader.load_migration(File.join(output_dir, UP_DIRNAME, migration_path))
|
39
|
+
|
40
|
+
statements.each do |statement|
|
41
|
+
conn.exec(statement)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def verify_bootstrap()
|
47
|
+
# come back in, and verify that the bootstrap tables are there
|
48
|
+
@dbutil.connect_test_database() do |conn|
|
49
|
+
conn.exec("SELECT table_name FROM information_schema.tables WHERE table_name = $1", [PG_MIGRATE_TABLE]) do |result|
|
50
|
+
result.ntuples.should == 1
|
51
|
+
result.getvalue(0, 0).should == PG_MIGRATE_TABLE
|
52
|
+
end
|
53
|
+
|
54
|
+
conn.exec("SELECT table_name FROM information_schema.tables WHERE table_name = $1", [PG_MIGRATIONS_TABLE]) do |result|
|
55
|
+
result.ntuples.should == 1
|
56
|
+
result.getvalue(0, 0).should == PG_MIGRATIONS_TABLE
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
single_manifest=File.expand_path('spec/pg_migrate/input_manifests/single_manifest')
|
63
|
+
single_manifest = File.join(single_manifest, '.')
|
64
|
+
|
65
|
+
input_dir = nil
|
66
|
+
target = Files.create :path => "target", :timestamp => false do
|
67
|
+
input_dir = dir "input_single_manifest", :src => single_manifest do
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
output_dir = File.join('target', 'output_single_manifest')
|
73
|
+
|
74
|
+
@standard_builder.build(input_dir, output_dir)
|
75
|
+
|
76
|
+
@dbutil.create_new_test_database()
|
77
|
+
|
78
|
+
# run bootstrap once, and verify the tables now exist
|
79
|
+
run_bootstrap(output_dir)
|
80
|
+
verify_bootstrap()
|
81
|
+
|
82
|
+
# run bootstrap again, and verify no error (implicitly), and that the tables now exist
|
83
|
+
run_bootstrap(output_dir)
|
84
|
+
verify_bootstrap()
|
85
|
+
|
86
|
+
# now run single1.sql
|
87
|
+
run_migration('single1.sql', output_dir)
|
88
|
+
|
89
|
+
@dbutil.connect_test_database() do |conn|
|
90
|
+
conn.exec("SELECT table_name FROM information_schema.tables WHERE table_name = $1", ["emp"]) do |result|
|
91
|
+
result.ntuples.should == 1
|
92
|
+
result.getvalue(0, 0).should == "emp"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# run it again. a very certain exception should occur... 'pg_migrate: code=migration_exists'
|
97
|
+
begin
|
98
|
+
run_migration('single1.sql', output_dir)
|
99
|
+
false.should == true
|
100
|
+
rescue Exception => e
|
101
|
+
e.message.index('pg_migrate: code=migration_exists').should_not == nil
|
102
|
+
end
|
103
|
+
|
104
|
+
@dbutil.connect_test_database() do |conn|
|
105
|
+
conn.exec("SELECT table_name FROM information_schema.tables WHERE table_name = $1", ["emp"]) do |result|
|
106
|
+
result.ntuples.should == 1
|
107
|
+
result.getvalue(0, 0).should == "emp"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ConfigParser do
|
4
|
+
it "parse my test database.yml file" do
|
5
|
+
config = ConfigParser.rails("spec/database.yml", "test")
|
6
|
+
config.should == {
|
7
|
+
:dbname => "pg_migrate_test",
|
8
|
+
:user => "postgres",
|
9
|
+
:password => "postgres",
|
10
|
+
:host => "localhost",
|
11
|
+
:port => 5432
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
it "run single migration" do
|
16
|
+
config = ConfigParser.rails("spec/database.yml", "test")
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module PgMigrate
|
2
|
+
|
3
|
+
class DbUtility
|
4
|
+
|
5
|
+
DEFAULT_OPTIONS = {
|
6
|
+
:dbtestname => "pg_migrate_test",
|
7
|
+
:dbsuperuser => "postgres",
|
8
|
+
:dbsuperpass => "postgres",
|
9
|
+
:dbhost => "localhost",
|
10
|
+
:dbport => 5432
|
11
|
+
}
|
12
|
+
|
13
|
+
def initialize(options=DEFAULT_OPTIONS)
|
14
|
+
|
15
|
+
options = DEFAULT_OPTIONS.merge(options)
|
16
|
+
|
17
|
+
@dbtestname = options[:dbtestname]
|
18
|
+
@dbsuperuser = options[:dbsuperuser]
|
19
|
+
@dbsuperpass = options[:dbsuperpass]
|
20
|
+
@dbhost = options[:dbhost]
|
21
|
+
@dbport = options[:dbport]
|
22
|
+
end
|
23
|
+
|
24
|
+
def pg_connection_hasher()
|
25
|
+
return {
|
26
|
+
:port => @dbport,
|
27
|
+
:user => @dbsuperuser,
|
28
|
+
:password => @dbsuperpass,
|
29
|
+
:host => @dbhost
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def create_new_test_database()
|
35
|
+
|
36
|
+
# this will presumably do the right default thing,
|
37
|
+
# to get us into a 'default' database where we can execute 'create database' from
|
38
|
+
conn_properties = pg_connection_hasher
|
39
|
+
|
40
|
+
conn_properties.delete(:dbname)
|
41
|
+
|
42
|
+
conn = PG::Connection.new(conn_properties)
|
43
|
+
|
44
|
+
conn.exec("DROP DATABASE IF EXISTS #{@dbtestname}").clear
|
45
|
+
conn.exec("CREATE DATABASE #{@dbtestname}").clear
|
46
|
+
|
47
|
+
conn.close
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
def connect_test_database(&block)
|
52
|
+
conn = nil
|
53
|
+
|
54
|
+
begin
|
55
|
+
conn_properties = pg_connection_hasher
|
56
|
+
|
57
|
+
conn_properties[:dbname] = @dbtestname
|
58
|
+
conn = PG::Connection.open(conn_properties)
|
59
|
+
|
60
|
+
yield conn
|
61
|
+
|
62
|
+
ensure
|
63
|
+
if !conn.nil?
|
64
|
+
conn.close
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
-- this is a SQL comment
|
2
|
+
|
3
|
+
select 1;
|
4
|
+
|
5
|
+
select 2;
|
6
|
+
|
7
|
+
select
|
8
|
+
3;
|
9
|
+
|
10
|
+
create table emp(id BIGSERIAL PRIMARY KEY, name varchar(255));
|
11
|
+
|
12
|
+
-- a sql function
|
13
|
+
CREATE FUNCTION clean_emp() RETURNS void AS '
|
14
|
+
DELETE FROM emp;
|
15
|
+
' LANGUAGE SQL;
|
16
|
+
|
17
|
+
-- a sql function on one line
|
18
|
+
CREATE FUNCTION clean_emp2() RETURNS void AS 'DELETE FROM emp;' LANGUAGE SQL;
|
19
|
+
|
20
|
+
CREATE FUNCTION populate() RETURNS integer AS $$
|
21
|
+
DECLARE
|
22
|
+
-- declarations
|
23
|
+
BEGIN
|
24
|
+
PERFORM clean_emp2();
|
25
|
+
END;
|
26
|
+
$$ LANGUAGE plpgsql;
|
27
|
+
|
28
|
+
|
29
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ManifestReader do
|
4
|
+
|
5
|
+
before(:all) do
|
6
|
+
@manifest_reader = ManifestReader.new
|
7
|
+
end
|
8
|
+
|
9
|
+
it "load single manifest" do
|
10
|
+
manifest = @manifest_reader.load_input_manifest("spec/pg_migrate/input_manifests/single_manifest")
|
11
|
+
|
12
|
+
manifest.length.should == 1
|
13
|
+
manifest[0].name.should == "single1.sql"
|
14
|
+
end
|
15
|
+
|
16
|
+
it "fail on bad manifest reference" do
|
17
|
+
expect { @manifest_reader.validate_migration_paths('absolutely_nowhere_real', ["migration1"]) }.to raise_exception
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Migrator do
|
4
|
+
|
5
|
+
before(:all) do
|
6
|
+
@manifest_reader = ManifestReader.new
|
7
|
+
@sql_reader = SqlReader.new
|
8
|
+
@standard_builder = Builder.new(@manifest_reader, @sql_reader)
|
9
|
+
@standard_migrator = Migrator.new(@manifest_reader, @sql_reader)
|
10
|
+
@dbutil = DbUtility.new
|
11
|
+
end
|
12
|
+
|
13
|
+
it "migrate single_manifest" do
|
14
|
+
|
15
|
+
def migrate_it(output_dir)
|
16
|
+
@dbutil.connect_test_database do |conn|
|
17
|
+
|
18
|
+
standard_migrator = Migrator.new(@manifest_reader, @sql_reader, :pgconn=>conn)
|
19
|
+
standard_migrator.migrate(output_dir)
|
20
|
+
|
21
|
+
conn.transaction do |transaction|
|
22
|
+
transaction.exec("SELECT table_name FROM information_schema.tables WHERE table_name = $1", ["emp"]) do |result|
|
23
|
+
result.ntuples.should == 1
|
24
|
+
result.getvalue(0, 0).should == "emp"
|
25
|
+
end
|
26
|
+
|
27
|
+
pg_migration_id = nil
|
28
|
+
transaction.exec("SELECT * FROM pg_migrations") do |result|
|
29
|
+
result.ntuples.should == 1
|
30
|
+
result[0]["name"].should == "single1.sql"
|
31
|
+
result[0]["ordinal"].should == "0"
|
32
|
+
pg_migration_id = result[0]["pg_migrate_id"]
|
33
|
+
end
|
34
|
+
pg_migration_id.should_not == nil
|
35
|
+
|
36
|
+
# verify that a database row in pg_migrate was created as side-effect
|
37
|
+
transaction.exec("SELECT * FROM pg_migrate WHERE id = $1", [pg_migration_id]) do |result|
|
38
|
+
result.ntuples.should == 1
|
39
|
+
result[0]["template_version"].should == "0.0.1"
|
40
|
+
result[0]["builder_version"].should == "pg_migrate_ruby-#{PgMigrate::VERSION}"
|
41
|
+
result[0]["migrator_version"].should == "pg_migrate_ruby-#{PgMigrate::VERSION}"
|
42
|
+
result[0]["database_version"].should_not be nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
single_manifest=File.expand_path('spec/pg_migrate/input_manifests/single_manifest')
|
49
|
+
single_manifest = File.join(single_manifest, '.')
|
50
|
+
|
51
|
+
input_dir = nil
|
52
|
+
target = Files.create :path => "target", :timestamp => false do
|
53
|
+
input_dir = dir "input_single_manifest", :src => single_manifest do
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
output_dir = File.join('target', 'output_single_manifest')
|
59
|
+
|
60
|
+
@standard_builder.build(input_dir, output_dir, :force => true)
|
61
|
+
|
62
|
+
@dbutil.create_new_test_database
|
63
|
+
|
64
|
+
migrate_it(output_dir)
|
65
|
+
migrate_it(output_dir)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SqlReader do
|
4
|
+
|
5
|
+
before(:all) do
|
6
|
+
@sql_reader = SqlReader.new
|
7
|
+
end
|
8
|
+
|
9
|
+
it "load single migration" do
|
10
|
+
migrations = @sql_reader.load_migration("spec/pg_migrate/input_manifests/single_manifest/up/single1.sql")
|
11
|
+
|
12
|
+
migrations.length.should == 7
|
13
|
+
migrations[0] = "select 1"
|
14
|
+
migrations[1] = "select 2"
|
15
|
+
migrations[2] = "select 3"
|
16
|
+
migrations[3] = "create table emp()"
|
17
|
+
migrations[4] = "CREATE FUNCTION clean_emp() RETURNS void AS ' DELETE FROM emp; ' LANGUAGE SQL"
|
18
|
+
migrations[5] = "CREATE FUNCTION clean_emp2() RETURNS void AS 'DELETE FROM emp;' LANGUAGE SQL"
|
19
|
+
migrations[6] = "CREATE FUNCTION populate() RETURNS integer AS $$ DECLARE BEGIN PERFORM select 1; END; $$ LANGUAGE plpgsql"
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'logging'
|
2
|
+
|
3
|
+
# bootstrap logger
|
4
|
+
Logging.logger.root.level = :debug
|
5
|
+
Logging.logger.root.appenders = Logging.appenders.stdout
|
6
|
+
|
7
|
+
require 'pg_migrate'
|
8
|
+
require 'pg_migrate/db_utility'
|
9
|
+
require 'files'
|
10
|
+
require 'fileutils'
|
11
|
+
|
12
|
+
target = File.join(File.dirname(__FILE__), '..', 'target')
|
13
|
+
FileUtils::rm_r(target, :force => true)
|
14
|
+
|
15
|
+
include PgMigrate
|