pg-schema-migration 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a6c40b5c1a5b55b983ddd433ba4ac78c6d9ba0f3a55f06a7472a6657dee28f98
4
+ data.tar.gz: 7d5d7d07dbc818d29e0a81ebd46cb041ac10e14af9f11cc699a397bea75f17ef
5
+ SHA512:
6
+ metadata.gz: ab53c65ef78c33f718ba5ce0d749bfafe8a24e113758e6b8295f244ed567f486c380c25c32b0213b6c4fe3aa77eb59e9e9cc500942e64af0a17947fff0ce87d1
7
+ data.tar.gz: 4385a5096b0d7db59f5011dce601a62d7e7706e20e933e13d8c9b6ba72204a40713a482036c75eb52dff538f262aa2941be368acb6c55f305d9b1ccdf318ea35
data/.gems ADDED
@@ -0,0 +1,2 @@
1
+ pg -v 1.0.0
2
+ pry -v 0.11.3
data/Dockerfile ADDED
@@ -0,0 +1,22 @@
1
+ FROM ruby:2.4.1-slim
2
+
3
+ MAINTAINER Alfredo Ramírez <alfredormz@gmail.com>
4
+
5
+ ENV PAGER="more"
6
+
7
+ RUN \
8
+ apt-get update && \
9
+ apt-get -y install build-essential libpq-dev
10
+
11
+ ENV HOME /root
12
+
13
+ WORKDIR /app
14
+
15
+ COPY .gems /tmp/
16
+
17
+ RUN \
18
+ gem install dep:1.1.0 && \
19
+ cd /tmp/ && \
20
+ dep -f .gems install && dep -f .gems && rm .gems
21
+
22
+ COPY . /app/
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2018 Alfredo Ramírez.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # PG Schema Migration
2
+
3
+ Simple schema migrations for PostgreSQL, which runs pure SQL. Inspired by [Sequel](https://github.com/jeremyevans/sequel) and [Cassandra Schema](https://github.com/tarolandia/cassandra-schema).
4
+
5
+ ## Installation
6
+
7
+ ```
8
+ gem install pg-schema-migration
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ PG::Schema uses a DSL via the `PG::Schema.migration` method, a migration must have an `up` block with the changes you want to apply to the schema, and a `down` block reversing the change made by `up`.
14
+
15
+ ### A basic Migration
16
+
17
+ Use `execute` inside `up` and `down` blocks to run the queries that will modify the schema. Here is a fairly basic `PG::Schema` migration.
18
+
19
+ ```ruby
20
+ PG:Schema.migration do
21
+ up do
22
+ execute <<~SQL
23
+ CREATE TABLE products (
24
+ name VARCHAR(30),
25
+ price NUMERIC
26
+ )
27
+ SQL
28
+
29
+ execute "CREATE TRIGGER..."
30
+ end
31
+
32
+ down do
33
+ execute "DROP TABLE products"
34
+ execute "DROP TRIGGER..."
35
+ end
36
+ end
37
+ ```
38
+ If there is an error while running a migration, it will rollback the previous schema changes made by the migration.
39
+
40
+ ### Running migrations
41
+
42
+ ```ruby
43
+ require 'pg/schema-migration'
44
+ require 'pg'
45
+
46
+ @db = PG.connect(ENV.fetch("DATABASE_URL"))
47
+
48
+ PG::Schema.migration {...}
49
+ PG::Schema.migration {...}
50
+
51
+ migrator = PG::Schema::Migrator.new(
52
+ db: @db, # a PG connection
53
+ directory: 'path/to/migrations', # default: nil
54
+ log: Logger.new('/dev/nul') # default: Logger.new(STDOUT)
55
+ )
56
+ ```
57
+ Migrate to the latest version
58
+
59
+ ```ruby
60
+ migrator.run!
61
+ ```
62
+ Migrate to an specific version
63
+
64
+ ```ruby
65
+ migrator.run!(version: 1)
66
+ ```
67
+ ### Migration files
68
+
69
+ `PG::Schemas::Migrator` expects that each migration file will be in a specific directory. For example:
70
+
71
+ ```ruby
72
+ PG::Schema::Migrator.new(db: @conn, directory: 'db/migrations').run!
73
+ ```
74
+
75
+ `PG::Schema::Migrator` will look in the `db/migrations` folder relative to the current directory, and run unapplied migrations on the database.
76
+
77
+ The migration files must be specified as follows:
78
+
79
+ ```bash
80
+ version_name.rb
81
+ ```
82
+
83
+ where `version` is an integer and `name` is a string which should be a very brief description of what the migration does. Examples:
84
+ ```bash
85
+ 001_create_films.rb
86
+ 002_add_director_to_films.rb
87
+ ...
88
+ 015_foo.rb
89
+ 016_bar.rb
90
+ ```
91
+ This guide is based on https://github.com/jeremyevans/sequel/blob/master/doc/migration.rdoc
92
+
93
+ # License
94
+ Copyright (c) 2018 Alfredo Ramírez.
95
+
96
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
97
+
98
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
99
+
100
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,22 @@
1
+ ---
2
+ version: '2'
3
+
4
+ services:
5
+ pg:
6
+ image: postgres
7
+ ports:
8
+ - "5432:5432"
9
+ environment:
10
+ POSTGRES_USER: postgres
11
+ POSTGRES_PASSWORD: postgres
12
+ POSTGRES_DB: pg-schema-migration-test
13
+ test:
14
+ command: tail -f /dev/null
15
+ image: pg-schema-migration:test
16
+ working_dir: /opt/test
17
+ volumes:
18
+ - ${PWD}:/opt/test
19
+ links:
20
+ - pg
21
+ environment:
22
+ DATABASE_URL: "postgresql://postgres:postgres@pg:5432/pg-schema-migration-test"
@@ -0,0 +1,164 @@
1
+ require 'pg'
2
+ require 'logger'
3
+
4
+ module PG
5
+ module Schema
6
+ @@migrations = []
7
+
8
+ MigrationNotFoundError = Class.new(Exception)
9
+
10
+ def self.migrations
11
+ @@migrations
12
+ end
13
+
14
+ def self.migration(&block)
15
+ MigrationDSL.new(&block).migration.tap do |migration|
16
+ @@migrations << migration
17
+ end
18
+ end
19
+
20
+ def self.get_migration_by_version(version)
21
+ migration_position = version - 1
22
+
23
+ raise MigrationNotFoundError, "can't find migration with version #{version}" unless @@migrations[migration_position]
24
+
25
+ @@migrations[migration_position]
26
+ end
27
+
28
+ class Migration
29
+ attr_accessor :up, :down
30
+
31
+ def initialize
32
+ @up = []
33
+ @down = []
34
+ end
35
+ end
36
+
37
+ class MigrationDSL < BasicObject
38
+ attr_reader :migration
39
+
40
+ def initialize(&block)
41
+ @migration = Migration.new
42
+ instance_eval(&block)
43
+ end
44
+
45
+ def up(&block)
46
+ @commands = []
47
+ @migration.up = block.call
48
+ end
49
+
50
+ def down(&block)
51
+ @commands = []
52
+ @migration.down = block.call
53
+ end
54
+
55
+ def execute(command)
56
+ @commands << command
57
+ end
58
+ end
59
+
60
+ class Migrator
61
+ attr_accessor :connection, :directory
62
+
63
+ MIGRATION_FILE_REGEX = /^(\d+)_.+\.rb/i
64
+
65
+ def initialize(db:, directory: nil, log: Logger.new(STDOUT))
66
+ @db = db
67
+ @directory = directory
68
+ @log = log
69
+
70
+ load_migration_files
71
+ generate_schema_migration_table!
72
+ end
73
+
74
+ def run!(version: nil)
75
+ migrations = PG::Schema.migrations
76
+ _current_version = current_version
77
+
78
+ version||= migrations.count
79
+
80
+ if version == _current_version || migrations.empty?
81
+ @log.info "Nothing to do."
82
+ return
83
+ end
84
+
85
+ t = Time.now
86
+ @log.info "Migrationg from #{_current_version} to version #{version}"
87
+ if version > _current_version
88
+ (_current_version + 1).upto(version) do |target|
89
+ apply(target, :up)
90
+ end
91
+ else
92
+ (_current_version).downto(version + 1) do |target|
93
+ apply(target, :down)
94
+ end
95
+ end
96
+
97
+ @log.info "Finished applying migration #{version}, took #{sprintf('%0.6f', Time.now - t)} seconds"
98
+ @log.info "Done!"
99
+ end
100
+
101
+ def apply(version, direction)
102
+ migration = PG::Schema.get_migration_by_version(version)
103
+ commands = migration.public_send(direction)
104
+
105
+ commands.reverse! if direction == :down
106
+
107
+ @db.transaction do
108
+ commands.each do |command|
109
+ @db.exec(command)
110
+ end
111
+ end
112
+
113
+ update_version(direction == :up ? version : version - 1)
114
+ rescue => error
115
+ @log.error error.message
116
+ @log.info "The migration failed. Current version #{current_version}"
117
+ raise
118
+ end
119
+
120
+ def load_migration_files
121
+ files = []
122
+
123
+ if directory
124
+ Dir.new(directory).sort.each do |file|
125
+ next unless file.match(MIGRATION_FILE_REGEX)
126
+ file = File.join(directory, file)
127
+ files << file
128
+ load(file)
129
+ end
130
+ end
131
+
132
+ files
133
+ end
134
+
135
+ def generate_schema_migration_table!
136
+ @db.exec <<~SQL
137
+ CREATE TABLE IF NOT EXISTS schema_information (
138
+ version INTEGER DEFAULT 0 NOT NULL
139
+ )
140
+ SQL
141
+
142
+ if @db.exec("SELECT * FROM schema_information").ntuples.zero?
143
+ @db.exec <<~SQL
144
+ INSERT INTO schema_information DEFAULT VALUES
145
+ SQL
146
+ end
147
+ end
148
+
149
+ def update_version(version)
150
+ @db.exec("UPDATE schema_information SET version = $1", [version])
151
+ end
152
+
153
+ def current_version
154
+ res = @db.exec <<~SQL
155
+ SELECT version FROM schema_information
156
+ SQL
157
+
158
+ raise "Schema information has multiple values" if res.ntuples > 1
159
+
160
+ res.field_values("version").first.to_i
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,14 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "pg-schema-migration"
3
+ s.version = "0.0.1"
4
+ s.summary = "PG Schema Migration"
5
+ s.description = "Simple schema migrations for PostgreSQL, which runs pure SQL."
6
+ s.authors = ["Alfredo Ramírez"]
7
+ s.email = ["alfredormz@gmail.com"]
8
+ s.homepage = "http://github.com/alfredormz/pg-schema-migration"
9
+ s.license = "MIT"
10
+ s.require_paths = ["lib"]
11
+ s.files = `git ls-files`.split("\n")
12
+
13
+ s.add_runtime_dependency "pg"
14
+ end
@@ -0,0 +1,17 @@
1
+ PG::Schema.migration do
2
+ up do
3
+ execute <<~SQL
4
+ CREATE TABLE users (
5
+ id integer,
6
+ name varchar(30),
7
+ created_at timestamp
8
+ )
9
+ SQL
10
+ end
11
+
12
+ down do
13
+ execute <<~SQL
14
+ DROP TABLE users
15
+ SQL
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ PG::Schema.migration do
2
+ up do
3
+ execute <<~SQL
4
+ CREATE TABLE films (
5
+ code varchar(5),
6
+ title varchar(30)
7
+ )
8
+ SQL
9
+
10
+ execute <<~SQL
11
+ ALTER TABLE users ADD COLUMN email varchar(40)
12
+ SQL
13
+ end
14
+
15
+ down do
16
+ execute "DROP TABLE films"
17
+ execute "ALTER TABLE users DROP COLUMN email"
18
+ end
19
+ end
File without changes
@@ -0,0 +1,190 @@
1
+ require 'minitest/autorun'
2
+ require_relative '../../lib/pg/schema-migration'
3
+
4
+ describe "PG::Schema" do
5
+ before do
6
+ PG::Schema.migrations.clear
7
+ end
8
+
9
+ describe "PG::Schema.migrations" do
10
+ it "returns an empty migrations array" do
11
+ assert_equal [], PG::Schema.migrations
12
+ end
13
+
14
+ it "should include migration instances created by migration DSL" do
15
+ migration = PG::Schema.migration {}
16
+
17
+ assert_equal PG::Schema::Migration, migration.class
18
+ assert_equal [migration], PG::Schema.migrations
19
+ end
20
+
21
+ it "should return migrations in order of creation" do
22
+ i1 = PG::Schema.migration {}
23
+ i2 = PG::Schema.migration {}
24
+ i3 = PG::Schema.migration {}
25
+
26
+ assert_equal [i1, i2, i3], PG::Schema.migrations
27
+ end
28
+
29
+ it "should get a singular migration by the version number" do
30
+ i1 = PG::Schema.migration {}
31
+ i2 = PG::Schema.migration {}
32
+ i3 = PG::Schema.migration {}
33
+
34
+ assert_equal i1, PG::Schema.get_migration_by_version(1)
35
+ assert_equal i2, PG::Schema.get_migration_by_version(2)
36
+ assert_equal i3, PG::Schema.get_migration_by_version(3)
37
+
38
+ assert_raises PG::Schema::MigrationNotFoundError do
39
+ PG::Schema.get_migration_by_version(4)
40
+ end
41
+ end
42
+ end
43
+
44
+ describe "DSL" do
45
+ it "should have default up and down that do nothing" do
46
+ m = PG::Schema.migration {}
47
+
48
+ assert_equal [], m.up
49
+ assert_equal [], m.down
50
+ end
51
+
52
+ it "should create migration with up and down commands" do
53
+ PG::Schema.migration do
54
+ up do
55
+ execute "SQL sentence #1"
56
+ execute "SQL sentence #2"
57
+ end
58
+
59
+ down do
60
+ execute "Revert SQL sentence #1"
61
+ execute "Revert SQL sentence #2"
62
+ end
63
+ end
64
+
65
+ migration = PG::Schema.migrations.first
66
+
67
+ assert_equal(
68
+ [
69
+ "SQL sentence #1",
70
+ "SQL sentence #2",
71
+ ],
72
+ migration.up,
73
+ )
74
+
75
+ assert_equal(
76
+ [
77
+ "Revert SQL sentence #1",
78
+ "Revert SQL sentence #2",
79
+ ],
80
+ migration.down,
81
+ )
82
+ end
83
+ end
84
+
85
+ describe "PG::Schema::Migrator" do
86
+ before do
87
+ @conn = PG.connect(ENV['DATABASE_URL'])
88
+
89
+ # Disable PostgreSQL notice messages
90
+ @conn.set_notice_receiver {}
91
+
92
+ @conn.exec("DROP TABLE IF EXISTS schema_information")
93
+ @conn.exec("DROP TABLE IF EXISTS users")
94
+ @conn.exec("DROP TABLE IF EXISTS films")
95
+
96
+ @migrator = PG::Schema::Migrator.new(
97
+ db: @conn,
98
+ directory: 'test/db/migrations',
99
+ log: Logger.new('/dev/null'),
100
+ )
101
+ end
102
+
103
+ it "should find and sort the migration files" do
104
+ assert_equal(
105
+ [
106
+ "test/db/migrations/001_create_table_users.rb",
107
+ "test/db/migrations/002_create_table_films.rb",
108
+ "test/db/migrations/015_empty_migration.rb",
109
+ ],
110
+ @migrator.load_migration_files,
111
+ )
112
+ end
113
+
114
+ it "should have '0' as current version" do
115
+ assert_equal 0, @migrator.current_version
116
+ end
117
+
118
+ it "should load the migration files as migrations" do
119
+ assert_equal 2, PG::Schema.migrations.size
120
+
121
+ # Add a new empty migration
122
+ PG::Schema.migration {}
123
+
124
+ assert_equal 3, PG::Schema.migrations.size
125
+ end
126
+
127
+ describe "migrating up" do
128
+ it "should migrates to the last version " do
129
+ @migrator.run!
130
+ assert_equal 2, @migrator.current_version
131
+ end
132
+
133
+ it "should executes the first migration" do
134
+ @migrator.run!(version: 1)
135
+ assert_equal 1, @migrator.current_version
136
+ end
137
+
138
+ it "should executes the 2nd migration" do
139
+ @migrator.run!(version: 2)
140
+ assert_equal 2, @migrator.current_version
141
+ end
142
+
143
+ it "should raise an exception if the version doen't exist" do
144
+ assert_raises PG::Schema::MigrationNotFoundError do
145
+ @migrator.run!(version: 4)
146
+ end
147
+ end
148
+
149
+ it "should not raise an exception with multiple runs " do
150
+ @migrator.run!
151
+ @migrator.run!
152
+ @migrator.run!
153
+
154
+ assert_equal 2, @migrator.current_version
155
+ end
156
+
157
+ it "should rollback the migration if a command failed" do
158
+ PG::Schema.migration do
159
+ up do
160
+ execute "CREATE TABLE test_users AS TABLE users"
161
+ execute "SOME FAULTY SQL"
162
+ end
163
+ end
164
+
165
+ assert_raises PG::SyntaxError do
166
+ @migrator.run!
167
+ end
168
+
169
+ assert_equal 3, PG::Schema.migrations.count
170
+ assert_equal 2, @migrator.current_version
171
+ end
172
+ end
173
+
174
+ describe "migrating down" do
175
+ before do
176
+ @migrator.run!
177
+ end
178
+
179
+ it "should migrates to the first version " do
180
+ @migrator.run!(version: 1)
181
+ assert_equal 1, @migrator.current_version
182
+ end
183
+
184
+ it "should reset the schema when the version is 0" do
185
+ @migrator.run!(version: 0)
186
+ assert_equal 0, @migrator.current_version
187
+ end
188
+ end
189
+ end
190
+ end
data/test/run ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env sh
2
+
3
+ cd "$(dirname "$0")"/..
4
+
5
+ docker build -t pg-schema-migration:test -f Dockerfile .
6
+
7
+ docker-compose -p pg-schema-migration \
8
+ -f docker-compose.yml run test \
9
+ sh -c "ruby test/pg/schema_migration_spec.rb"
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pg-schema-migration
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alfredo Ramírez
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-05-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pg
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Simple schema migrations for PostgreSQL, which runs pure SQL.
28
+ email:
29
+ - alfredormz@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".gems"
35
+ - Dockerfile
36
+ - LICENSE
37
+ - README.md
38
+ - docker-compose.yml
39
+ - lib/pg/schema-migration.rb
40
+ - pg-schema-migration.gemspec
41
+ - test/db/migrations/001_create_table_users.rb
42
+ - test/db/migrations/002_create_table_films.rb
43
+ - test/db/migrations/015_empty_migration.rb
44
+ - test/pg/schema_migration_spec.rb
45
+ - test/run
46
+ homepage: http://github.com/alfredormz/pg-schema-migration
47
+ licenses:
48
+ - MIT
49
+ metadata: {}
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubyforge_project:
66
+ rubygems_version: 2.7.4
67
+ signing_key:
68
+ specification_version: 4
69
+ summary: PG Schema Migration
70
+ test_files: []