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
@@ -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
|