pg_ha_migrations 0.1.3
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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +213 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +11 -0
- data/lib/pg_ha_migrations.rb +32 -0
- data/lib/pg_ha_migrations/allowed_versions.rb +23 -0
- data/lib/pg_ha_migrations/blocking_database_transactions.rb +54 -0
- data/lib/pg_ha_migrations/blocking_database_transactions_reporter.rb +52 -0
- data/lib/pg_ha_migrations/railtie.rb +5 -0
- data/lib/pg_ha_migrations/safe_statements.rb +192 -0
- data/lib/pg_ha_migrations/unsafe_statements.rb +79 -0
- data/lib/pg_ha_migrations/version.rb +3 -0
- data/lib/tasks/blocking_transactions.rake +9 -0
- data/pg_ha_migrations.gemspec +33 -0
- metadata +169 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 764006f9563011eaf0c11d51dfdac477b273f6e63687df9d9448861129640615
|
4
|
+
data.tar.gz: 2ddde5f6e1e39ab07034ff6d43bdd5a9c5f0b71ab75b6c9a5100f4ebfd3c2bec
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 993b63e38c85ae14660d300e122c3ad6d748e88d12a62182624e1b80b18862a61fe337c88ecfca7e0e149f64b575d08ec09248972e3dc44fab844d7bc03a5954
|
7
|
+
data.tar.gz: 2bdccc2af3fa5e6834a31761c7e19ef4dc1983f5a84dc2b7f7dd8c188ac3c6a2e5ff745de1907bc2ff66c92976668f070eedabb40e08acb5bfe6d02617f8c6eb
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.2.3
|
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at code@getbraintree.com. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: http://contributor-covenant.org
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Braintree Payments
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,213 @@
|
|
1
|
+
# PgHaMigrations
|
2
|
+
|
3
|
+
PgHaMigrations is a gem that applies learned best practices to ActiveRecord Migrations.
|
4
|
+
|
5
|
+
Provided functionality:
|
6
|
+
- [Migrations](#migrations)
|
7
|
+
- [Utilities](#utilities)
|
8
|
+
- [Rake Tasks](#rake-tasks)
|
9
|
+
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'pg_ha_migrations'
|
17
|
+
```
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install pg_ha_migrations
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
### Migrations
|
30
|
+
|
31
|
+
In general, existing migrations are prefixed with `unsafe_` and safer alternatives are provided prefixed with `safe_`.
|
32
|
+
|
33
|
+
Migrations prefixed with `unsafe_` will warn when invoked. The API is designed to be explicit yet remain flexible. There may be situations where invoking the unsafe migration is preferred.
|
34
|
+
|
35
|
+
Migrations prefixed with `safe_` prefer concurrent operations where available, set low lock timeouts where appropriate, and decompose operations into multiple safe steps.
|
36
|
+
|
37
|
+
The following functionality is currently unsupported:
|
38
|
+
|
39
|
+
- Rollbacks
|
40
|
+
- Generators
|
41
|
+
- schema.rb
|
42
|
+
|
43
|
+
#### safe\_create\_table
|
44
|
+
|
45
|
+
Safely creates a new table.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
safe_create_table :table do |t|
|
49
|
+
t.type :column
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
#### safe\_create\_enum\_type
|
54
|
+
|
55
|
+
Safely create a new enum without values.
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
safe_create_enum_type :enum
|
59
|
+
```
|
60
|
+
Or, safely create the enum with values.
|
61
|
+
```ruby
|
62
|
+
safe_create_enum_type :enum, ["value1", "value2"]
|
63
|
+
```
|
64
|
+
|
65
|
+
#### safe\_add\_enum\_value
|
66
|
+
|
67
|
+
Safely add a new enum value.
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
safe_add_enum_value :enum, "value"
|
71
|
+
```
|
72
|
+
|
73
|
+
#### safe\_add\_column
|
74
|
+
|
75
|
+
Safely add a column.
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
safe_add_column :table, :column, :type
|
79
|
+
```
|
80
|
+
|
81
|
+
#### unsafe\_add\_column
|
82
|
+
|
83
|
+
Unsafely add a column, but do so with a lock that is safely acquired.
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
unsafe_add_column :table, :column, :type
|
87
|
+
```
|
88
|
+
|
89
|
+
#### safe\_change\_column\_default
|
90
|
+
|
91
|
+
Safely change the default value for a column.
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
safe_change_column_default :table, :column, "value"
|
95
|
+
```
|
96
|
+
|
97
|
+
#### safe\_make\_column\_nullable
|
98
|
+
|
99
|
+
Safely make the column nullable.
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
safe_make_column_nullable :table, :column
|
103
|
+
```
|
104
|
+
|
105
|
+
#### unsafe\_make\_column\_not\_nullable
|
106
|
+
|
107
|
+
Unsafely make a column not nullable.
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
unsafe_make_column_not_nullable :table, :column
|
111
|
+
```
|
112
|
+
|
113
|
+
#### safe\_add\_concurrent\_index
|
114
|
+
|
115
|
+
Add an index concurrently. Migrations that contain this statement must also include `disable_ddl_transaction!`.
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
safe_add_concurrent_index :table, :column
|
119
|
+
```
|
120
|
+
|
121
|
+
Add a composite btree index.
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
safe_add_concurrent_index :table, [:column1, :column2], name: "index_name", using: :btree
|
125
|
+
```
|
126
|
+
|
127
|
+
#### safe\_remove\_concurrent\_index
|
128
|
+
|
129
|
+
Safely remove an index. Migrations that contain this statement must also include `disable_ddl_transaction!`.
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
safe_remove_concurrent_index :table, :name => :index_name
|
133
|
+
```
|
134
|
+
|
135
|
+
|
136
|
+
### Utilities
|
137
|
+
|
138
|
+
#### safely\_acquire\_lock\_for\_table
|
139
|
+
|
140
|
+
Safely acquire a lock for a table.
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
safely_acquire_lock_for_table(:table) do
|
144
|
+
...
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
148
|
+
#### adjust\_lock\_timeout
|
149
|
+
|
150
|
+
Adjust lock timeout.
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
adjust_lock_timeout(seconds) do
|
154
|
+
...
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
158
|
+
#### adjust\_statement\_timeout
|
159
|
+
|
160
|
+
Adjust statement timeout.
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
adjust_statement_timeout(seconds) do
|
164
|
+
...
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
#### safe\_set\_maintenance\_work\_mem\_gb
|
169
|
+
|
170
|
+
Set maintenance work mem.
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
safe_set_maintenance_work_mem_gb 1
|
174
|
+
```
|
175
|
+
|
176
|
+
|
177
|
+
### Rake Tasks
|
178
|
+
|
179
|
+
Use this to check for blocking transactions before migrating.
|
180
|
+
|
181
|
+
$ bundle exec rake pg_ha_migrations:check_blocking_database_transactions
|
182
|
+
|
183
|
+
This rake task expects that you already have a connection open to your database. We suggest that you add another rake task to open the connection and then add that as a prerequisite for `pg_ha_migrations:check_blocking_database_transactions`.
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
namespace :db do
|
187
|
+
desc "Establish a database connection"
|
188
|
+
task :establish_connection do
|
189
|
+
ActiveRecord::Base.establish_connection
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
Rake::Task["pg_ha_migrations:check_blocking_database_transactions"].enhance ["db:establish_connection"]
|
194
|
+
```
|
195
|
+
|
196
|
+
|
197
|
+
## Development
|
198
|
+
|
199
|
+
After checking out the repo, run `bin/setup` to install dependencies and start a postgres docker container. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
200
|
+
|
201
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
202
|
+
|
203
|
+
## Contributing
|
204
|
+
|
205
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/braintreeps/pg_ha_migrations. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
206
|
+
|
207
|
+
## License
|
208
|
+
|
209
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
210
|
+
|
211
|
+
## Code of Conduct
|
212
|
+
|
213
|
+
Everyone interacting in the PgHaMigrations project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/braintreeps/pg_ha_migrations/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "pg_ha_migrations"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require "pg_ha_migrations/version"
|
2
|
+
require "rails"
|
3
|
+
require "active_record"
|
4
|
+
require "active_record/migration"
|
5
|
+
require "relation_to_struct"
|
6
|
+
|
7
|
+
module PgHaMigrations
|
8
|
+
LOCK_TIMEOUT_SECONDS = 5
|
9
|
+
LOCK_FAILURE_RETRY_DELAY_MULTLIPLIER = 5
|
10
|
+
|
11
|
+
# Safe versus unsafe in this context specifically means the following:
|
12
|
+
# - Safe operations will not block for long periods of time.
|
13
|
+
# - Unsafe operations _may_ block for long periods of time.
|
14
|
+
UnsafeMigrationError = Class.new(Exception)
|
15
|
+
|
16
|
+
# Invalid migrations are operations which we expect to put the schema
|
17
|
+
# into a state inconsistent with our current guidelines; e.g., using
|
18
|
+
# a 32-bit foreign key value when the referenced primary key column
|
19
|
+
# is a 64-bit value.
|
20
|
+
InvalidMigrationError = Class.new(Exception)
|
21
|
+
|
22
|
+
# This gem only supports the PostgreSQL adapter at this time.
|
23
|
+
UnsupportedAdapter = Class.new(Exception)
|
24
|
+
end
|
25
|
+
|
26
|
+
require "pg_ha_migrations/blocking_database_transactions"
|
27
|
+
require "pg_ha_migrations/blocking_database_transactions_reporter"
|
28
|
+
require "pg_ha_migrations/unsafe_statements"
|
29
|
+
require "pg_ha_migrations/safe_statements"
|
30
|
+
require "pg_ha_migrations/allowed_versions"
|
31
|
+
require "pg_ha_migrations/railtie"
|
32
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "active_record/migration/compatibility"
|
2
|
+
|
3
|
+
module PgHaMigrations::AllowedVersions
|
4
|
+
ALLOWED_VERSIONS = [4.2, 5.0, 5.1].map do |v|
|
5
|
+
begin
|
6
|
+
ActiveRecord::Migration[v]
|
7
|
+
rescue ArgumentError
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
end.compact
|
11
|
+
|
12
|
+
def inherited(subclass)
|
13
|
+
super
|
14
|
+
unless ALLOWED_VERSIONS.include?(subclass.superclass)
|
15
|
+
raise StandardError, "#{subclass.superclass} is not a permitted migration class\n" \
|
16
|
+
"\n" \
|
17
|
+
"To add a new version update the ALLOWED_VERSIONS constant in #{__FILE__}\n" \
|
18
|
+
"Currently allowed versions: #{ALLOWED_VERSIONS.map { |v| "ActiveRecord::Migration[#{v.current_version}]" }.join(', ')}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
ActiveRecord::Migration.singleton_class.send(:prepend, PgHaMigrations::AllowedVersions)
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module PgHaMigrations
|
2
|
+
class BlockingDatabaseTransactions
|
3
|
+
LongRunningTransaction = Struct.new(:database, :current_query, :transaction_age, :tables_with_locks) do
|
4
|
+
def description
|
5
|
+
"#{database} | tables (#{tables_with_locks.join(', ')}) have been locked for #{transaction_age} | query: #{current_query}"
|
6
|
+
end
|
7
|
+
|
8
|
+
def concurrent_index_creation?
|
9
|
+
!!current_query.match(/create\s+index\s+concurrently/i)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.autovacuum_regex
|
14
|
+
"^autovacuum: (?!.*to prevent wraparound)"
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.find_blocking_transactions(minimum_transaction_age = "0 seconds")
|
18
|
+
pid_column, state_column = if ActiveRecord::Base.connection.select_value("SHOW server_version") =~ /9\.1/
|
19
|
+
["procpid", "current_query"]
|
20
|
+
else
|
21
|
+
["pid", "query"]
|
22
|
+
end
|
23
|
+
|
24
|
+
raw_query = <<-SQL
|
25
|
+
SELECT
|
26
|
+
psa.datname as database, -- Will only ever be one database
|
27
|
+
psa.#{state_column} as current_query,
|
28
|
+
clock_timestamp() - psa.xact_start AS transaction_age,
|
29
|
+
array_agg(distinct c.relname) AS tables_with_locks
|
30
|
+
FROM pg_stat_activity psa -- Cluster wide
|
31
|
+
JOIN pg_locks l ON (psa.#{pid_column} = l.pid) -- Cluster wide
|
32
|
+
JOIN pg_class c ON (l.relation = c.oid) -- Database wide
|
33
|
+
JOIN pg_namespace ns ON (c.relnamespace = ns.oid) -- Database wide
|
34
|
+
WHERE psa.#{pid_column} != pg_backend_pid()
|
35
|
+
AND ns.nspname != 'pg_catalog'
|
36
|
+
AND c.relkind = 'r'
|
37
|
+
AND psa.xact_start < clock_timestamp() - ?::interval
|
38
|
+
AND psa.#{state_column} !~ ?
|
39
|
+
AND (
|
40
|
+
-- Be explicit about this being for a single database -- it's already implicit in
|
41
|
+
-- the relations used, and if we don't restrict this we could get incorrect results
|
42
|
+
-- with oid collisions from pg_namespace and pg_class.
|
43
|
+
l.database = 0
|
44
|
+
OR l.database = (SELECT d.oid FROM pg_database d WHERE d.datname = current_database())
|
45
|
+
)
|
46
|
+
GROUP BY psa.datname, psa.#{state_column}, psa.xact_start
|
47
|
+
SQL
|
48
|
+
|
49
|
+
query = ActiveRecord::Base.send(:sanitize_sql_for_conditions, [raw_query, minimum_transaction_age, autovacuum_regex])
|
50
|
+
|
51
|
+
ActiveRecord::Base.structs_from_sql(LongRunningTransaction, query)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
module PgHaMigrations
|
4
|
+
class BlockingDatabaseTransactionsReporter
|
5
|
+
CHECK_DURATION = "30 seconds"
|
6
|
+
|
7
|
+
def self.run
|
8
|
+
blocking_transactions = get_blocking_transactions
|
9
|
+
has_transactions = blocking_transactions.values.flatten.present?
|
10
|
+
_puts(report(blocking_transactions)) if has_transactions
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.report(transactions)
|
14
|
+
report = StringIO.new
|
15
|
+
report << "Potentially blocking transactions:\n"
|
16
|
+
transactions.each do |db_description, blocking_transactions|
|
17
|
+
report << "#{db_description}:\n"
|
18
|
+
if blocking_transactions.empty?
|
19
|
+
report << "\t(no long running transactions)\n\n"
|
20
|
+
else
|
21
|
+
blocking_transactions.each do |transaction|
|
22
|
+
report << "\t#{transaction.description}\n\n"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
if blocking_transactions.any?(&:concurrent_index_creation?)
|
27
|
+
report << <<-eos.strip_heredoc.lines.map { |line| "\t#{line}" }.join
|
28
|
+
Warning: concurrent indexes are currently being built. If you have any other
|
29
|
+
migrations in this deploy that will attempt to create additional
|
30
|
+
concurrent indexes on the same physical database (even if the table
|
31
|
+
being indexes is on another dimension) those migrations will not be
|
32
|
+
able to complete until the in-progress index creations finish.
|
33
|
+
|
34
|
+
For more information, see #service-db in Slack.\n
|
35
|
+
eos
|
36
|
+
report << "\n" # Blank line intentional
|
37
|
+
end
|
38
|
+
end
|
39
|
+
report.string
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.get_blocking_transactions
|
43
|
+
{
|
44
|
+
"Primary database" => PgHaMigrations::BlockingDatabaseTransactions.find_blocking_transactions(CHECK_DURATION)
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def self._puts(msg)
|
49
|
+
puts msg
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
module PgHaMigrations::SafeStatements
|
2
|
+
def safe_create_table(table, options={}, &block)
|
3
|
+
unsafe_create_table(table, options, &block)
|
4
|
+
end
|
5
|
+
|
6
|
+
def safe_create_enum_type(name, values=nil)
|
7
|
+
case values
|
8
|
+
when nil
|
9
|
+
raise ArgumentError, "safe_create_enum_type expects a set of values; if you want an enum with no values please pass an empty array"
|
10
|
+
when []
|
11
|
+
unsafe_execute("CREATE TYPE #{PG::Connection.quote_ident(name.to_s)} AS ENUM ()")
|
12
|
+
else
|
13
|
+
escaped_values = values.map do |value|
|
14
|
+
"'#{PG::Connection.escape_string(value.to_s)}'"
|
15
|
+
end
|
16
|
+
unsafe_execute("CREATE TYPE #{PG::Connection.quote_ident(name.to_s)} AS ENUM (#{escaped_values.join(',')})")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def safe_add_enum_value(name, value)
|
21
|
+
unsafe_execute("ALTER TYPE #{PG::Connection.quote_ident(name.to_s)} ADD VALUE '#{PG::Connection.escape_string(value)}'")
|
22
|
+
end
|
23
|
+
|
24
|
+
def safe_add_column(table, column, type, options = {})
|
25
|
+
if options.has_key? :default
|
26
|
+
raise PgHaMigrations::UnsafeMigrationError.new(":default is NOT SAFE! Use safe_change_column_default afterwards then backfill the data to prevent locking the table")
|
27
|
+
end
|
28
|
+
if options[:null] == false
|
29
|
+
raise PgHaMigrations::UnsafeMigrationError.new(":null => false is NOT SAFE if the table has data! If you _really_ want to do this, use unsafe_make_column_not_nullable")
|
30
|
+
end
|
31
|
+
|
32
|
+
unsafe_add_column(table, column, type, options)
|
33
|
+
end
|
34
|
+
|
35
|
+
def unsafe_add_column(table, column, type, options = {})
|
36
|
+
safely_acquire_lock_for_table(table) do
|
37
|
+
super(table, column, type, options)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def safe_change_column_default(table, column, default_value)
|
42
|
+
escaped_value = case default_value
|
43
|
+
when Proc
|
44
|
+
PG::Connection.escape_string(default_value.call.to_s)
|
45
|
+
else
|
46
|
+
"'#{PG::Connection.escape_string(default_value.to_s)}'"
|
47
|
+
end
|
48
|
+
|
49
|
+
safely_acquire_lock_for_table(table) do
|
50
|
+
unsafe_execute <<-SQL.strip_heredoc
|
51
|
+
ALTER TABLE #{PG::Connection.quote_ident(table.to_s)}
|
52
|
+
ALTER COLUMN #{PG::Connection.quote_ident(column.to_s)}
|
53
|
+
SET DEFAULT #{escaped_value}
|
54
|
+
SQL
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def safe_make_column_nullable(table, column)
|
59
|
+
safely_acquire_lock_for_table(table) do
|
60
|
+
unsafe_execute "ALTER TABLE #{table} ALTER COLUMN #{column} DROP NOT NULL"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def unsafe_make_column_not_nullable(table, column, options={}) # options arg is only present for backwards compatiblity
|
65
|
+
safely_acquire_lock_for_table(table) do
|
66
|
+
unsafe_execute "ALTER TABLE #{table} ALTER COLUMN #{column} SET NOT NULL"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def safe_add_concurrent_index(table, columns, options={})
|
71
|
+
unsafe_add_index(table, columns, options.merge(:algorithm => :concurrently))
|
72
|
+
end
|
73
|
+
|
74
|
+
def safe_remove_concurrent_index(table, options={})
|
75
|
+
unless options.is_a?(Hash) && options.key?(:name)
|
76
|
+
raise ArgumentError, "Expected safe_remove_concurrent_index to be called with arguments (table_name, :name => ...)"
|
77
|
+
end
|
78
|
+
unless ActiveRecord::Base.connection.postgresql_version >= 90600
|
79
|
+
raise PgHaMigrations::InvalidMigrationError, "Removing an index concurrently is not supported on Postgres 9.1 databases"
|
80
|
+
end
|
81
|
+
index_size = select_value("SELECT pg_size_pretty(pg_relation_size('#{options[:name]}'))")
|
82
|
+
say "Preparing to drop index #{options[:name]} which is #{index_size} on disk..."
|
83
|
+
unsafe_remove_index(table, options.merge(:algorithm => :concurrently))
|
84
|
+
end
|
85
|
+
|
86
|
+
def safe_set_maintenance_work_mem_gb(gigabytes)
|
87
|
+
unsafe_execute("SET maintenance_work_mem = '#{PG::Connection.escape_string(gigabytes.to_s)} GB'")
|
88
|
+
end
|
89
|
+
|
90
|
+
def _per_migration_caller
|
91
|
+
@_per_migration_caller ||= Kernel.caller
|
92
|
+
end
|
93
|
+
|
94
|
+
def _check_postgres_adapter!
|
95
|
+
expected_adapter = "PostgreSQL"
|
96
|
+
actual_adapter = ActiveRecord::Base.connection.adapter_name
|
97
|
+
raise PgHaMigrations::UnsupportedAdapter, "This gem only works with the #{expected_adapter} adapter, found #{actual_adapter} instead" unless actual_adapter == expected_adapter
|
98
|
+
end
|
99
|
+
|
100
|
+
def exec_migration(conn, direction)
|
101
|
+
_check_postgres_adapter!
|
102
|
+
super(conn, direction)
|
103
|
+
end
|
104
|
+
|
105
|
+
def safely_acquire_lock_for_table(table, &block)
|
106
|
+
_check_postgres_adapter!
|
107
|
+
table = table.to_s
|
108
|
+
quoted_table_name = connection.quote_table_name(table)
|
109
|
+
|
110
|
+
successfully_acquired_lock = false
|
111
|
+
|
112
|
+
until successfully_acquired_lock
|
113
|
+
while (
|
114
|
+
blocking_transactions = PgHaMigrations::BlockingDatabaseTransactions.find_blocking_transactions("#{PgHaMigrations::LOCK_TIMEOUT_SECONDS} seconds")
|
115
|
+
blocking_transactions.any? { |query| query.tables_with_locks.include?(table) }
|
116
|
+
)
|
117
|
+
say "Waiting on blocking transactions:"
|
118
|
+
blocking_transactions.each do |blocking_transaction|
|
119
|
+
say blocking_transaction.description
|
120
|
+
end
|
121
|
+
sleep(PgHaMigrations::LOCK_TIMEOUT_SECONDS)
|
122
|
+
end
|
123
|
+
|
124
|
+
connection.transaction do
|
125
|
+
adjust_timeout_method = connection.postgresql_version >= 90300 ? :adjust_lock_timeout : :adjust_statement_timeout
|
126
|
+
begin
|
127
|
+
method(adjust_timeout_method).call(PgHaMigrations::LOCK_TIMEOUT_SECONDS) do
|
128
|
+
connection.execute("LOCK #{quoted_table_name};")
|
129
|
+
end
|
130
|
+
successfully_acquired_lock = true
|
131
|
+
rescue ActiveRecord::StatementInvalid => e
|
132
|
+
if e.message =~ /PG::LockNotAvailable.+ lock timeout/ || e.message =~ /PG::QueryCanceled.+ statement timeout/
|
133
|
+
sleep_seconds = PgHaMigrations::LOCK_FAILURE_RETRY_DELAY_MULTLIPLIER * PgHaMigrations::LOCK_TIMEOUT_SECONDS
|
134
|
+
say "Timed out trying to acquire an exclusive lock on the #{quoted_table_name} table."
|
135
|
+
say "Sleeping for #{sleep_seconds}s to allow potentially queued up queries to finish before continuing."
|
136
|
+
sleep(sleep_seconds)
|
137
|
+
|
138
|
+
raise ActiveRecord::Rollback
|
139
|
+
else
|
140
|
+
raise e
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
if successfully_acquired_lock
|
145
|
+
block.call
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def adjust_lock_timeout(timeout_seconds = PgHaMigrations::LOCK_TIMEOUT_SECONDS, &block)
|
152
|
+
_check_postgres_adapter!
|
153
|
+
original_timeout = ActiveRecord::Base.value_from_sql("SHOW lock_timeout").sub(/s\Z/, '').to_i * 1000
|
154
|
+
begin
|
155
|
+
connection.execute("SET lock_timeout = #{PG::Connection.escape_string((timeout_seconds * 1000).to_s)};")
|
156
|
+
block.call
|
157
|
+
ensure
|
158
|
+
begin
|
159
|
+
connection.execute("SET lock_timeout = #{original_timeout};")
|
160
|
+
rescue ActiveRecord::StatementInvalid => e
|
161
|
+
if e.message =~ /PG::InFailedSqlTransaction/
|
162
|
+
# If we're in a failed transaction the `SET lock_timeout` will be rolled back,
|
163
|
+
# so we don't need to worry about cleaning up, and we can't execute SQL anyway.
|
164
|
+
else
|
165
|
+
raise e
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def adjust_statement_timeout(timeout_seconds, &block)
|
172
|
+
_check_postgres_adapter!
|
173
|
+
original_timeout = ActiveRecord::Base.value_from_sql("SHOW statement_timeout").sub(/s\Z/, '').to_i * 1000
|
174
|
+
begin
|
175
|
+
connection.execute("SET statement_timeout = #{PG::Connection.escape_string((timeout_seconds * 1000).to_s)};")
|
176
|
+
block.call
|
177
|
+
ensure
|
178
|
+
begin
|
179
|
+
connection.execute("SET statement_timeout = #{original_timeout};")
|
180
|
+
rescue ActiveRecord::StatementInvalid => e
|
181
|
+
if e.message =~ /PG::InFailedSqlTransaction/
|
182
|
+
# If we're in a failed transaction the `SET lock_timeout` will be rolled back,
|
183
|
+
# so we don't need to worry about cleaning up, and we can't execute SQL anyway.
|
184
|
+
else
|
185
|
+
raise e
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
ActiveRecord::Migration.send(:prepend, PgHaMigrations::SafeStatements)
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module PgHaMigrations::UnsafeStatements
|
2
|
+
def self.delegate_unsafe_method_to_connection(method_name)
|
3
|
+
define_method("unsafe_#{method_name}") do |*args, &block|
|
4
|
+
arg_list = args.map { |arg| arg.inspect }.join(', ')
|
5
|
+
# say_with_time args taken from https://github.com/rails/rails/blob/4-2-stable/activerecord/lib/active_record/migration.rb#L654
|
6
|
+
say_with_time "#{method_name}(#{arg_list})" do
|
7
|
+
connection.send(method_name, *args, &block)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
delegate_unsafe_method_to_connection :create_table
|
13
|
+
def create_table(name, options={})
|
14
|
+
raise PgHaMigrations::UnsafeMigrationError.new(":create_table is NOT SAFE! Use safe_create_table instead")
|
15
|
+
end
|
16
|
+
|
17
|
+
delegate_unsafe_method_to_connection :add_column
|
18
|
+
def add_column(table, column, type, options={})
|
19
|
+
raise PgHaMigrations::UnsafeMigrationError.new(":add_column is NOT SAFE! Use safe_add_column instead")
|
20
|
+
end
|
21
|
+
|
22
|
+
delegate_unsafe_method_to_connection :change_table
|
23
|
+
def change_table(name, options={})
|
24
|
+
raise PgHaMigrations::UnsafeMigrationError.new(":change_table is NOT SAFE! Use a combination of safe and explicit unsafe migration methods instead")
|
25
|
+
end
|
26
|
+
|
27
|
+
delegate_unsafe_method_to_connection :drop_table
|
28
|
+
def drop_table(name)
|
29
|
+
raise PgHaMigrations::UnsafeMigrationError.new(":drop_table is NOT SAFE! Explicitly call :unsafe_drop_table to proceed")
|
30
|
+
end
|
31
|
+
|
32
|
+
delegate_unsafe_method_to_connection :rename_table
|
33
|
+
def rename_table(old_name, new_name)
|
34
|
+
raise PgHaMigrations::UnsafeMigrationError.new(":rename_table is NOT SAFE! Explicitly call :unsafe_rename_table to proceed")
|
35
|
+
end
|
36
|
+
|
37
|
+
delegate_unsafe_method_to_connection :rename_column
|
38
|
+
def rename_column(table_name, column_name, new_column_name)
|
39
|
+
raise PgHaMigrations::UnsafeMigrationError.new(":rename_column is NOT SAFE! Explicitly call :unsafe_rename_column to proceed")
|
40
|
+
end
|
41
|
+
|
42
|
+
delegate_unsafe_method_to_connection :change_column
|
43
|
+
def change_column(table_name, column_name, type, options={})
|
44
|
+
raise PgHaMigrations::UnsafeMigrationError.new(":change_column is NOT SAFE! Use a combination of safe and explicit unsafe migration methods instead")
|
45
|
+
end
|
46
|
+
|
47
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
48
|
+
raise PgHaMigrations::UnsafeMigrationError.new(<<-EOS.strip_heredoc)
|
49
|
+
:change_column_null is NOT (guaranteed to be) SAFE! Either use :safe_make_column_nullable or explicitly call :unsafe_make_column_not_nullable to proceed
|
50
|
+
EOS
|
51
|
+
end
|
52
|
+
|
53
|
+
delegate_unsafe_method_to_connection :remove_column
|
54
|
+
def remove_column(table_name, column_name, type, options={})
|
55
|
+
raise PgHaMigrations::UnsafeMigrationError.new(":remove_column is NOT SAFE! Explicitly call :unsafe_remove_column to proceed")
|
56
|
+
end
|
57
|
+
|
58
|
+
delegate_unsafe_method_to_connection :add_index
|
59
|
+
def add_index(table_name, column_names, options={})
|
60
|
+
raise PgHaMigrations::UnsafeMigrationError.new(":add_index is NOT SAFE! Use safe_add_concurrent_index instead")
|
61
|
+
end
|
62
|
+
|
63
|
+
delegate_unsafe_method_to_connection :execute
|
64
|
+
def execute(sql, name = nil)
|
65
|
+
raise PgHaMigrations::UnsafeMigrationError.new(":execute is NOT SAFE! Explicitly call :unsafe_execute to proceed")
|
66
|
+
end
|
67
|
+
|
68
|
+
delegate_unsafe_method_to_connection :remove_index
|
69
|
+
def remove_index(table_name, options={})
|
70
|
+
raise PgHaMigrations::UnsafeMigrationError.new(":remove_index is NOT SAFE! Use safe_remove_concurrent_index instead for Postgres 9.6 databases; Explicitly call :unsafe_remove_index to proceed on Postgres 9.1")
|
71
|
+
end
|
72
|
+
|
73
|
+
delegate_unsafe_method_to_connection :add_foreign_key
|
74
|
+
def add_foreign_key(from_table, to_table, options)
|
75
|
+
raise PgHaMigrations::UnsafeMigrationError.new(":add_foreign_key is NOT SAFE! Explicitly call :unsafe_add_foreign_key only if you have guidance from a migration reviewer in #service-app-db.")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
ActiveRecord::Migration.send(:prepend, PgHaMigrations::UnsafeStatements)
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "pg_ha_migrations/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pg_ha_migrations"
|
8
|
+
spec.version = PgHaMigrations::VERSION
|
9
|
+
spec.authors = ["jcoleman"]
|
10
|
+
spec.email = ["code@getbraintree.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{}
|
13
|
+
spec.description = %q{}
|
14
|
+
spec.homepage = ""
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = "exe"
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.15"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
27
|
+
spec.add_development_dependency "pg"
|
28
|
+
spec.add_development_dependency "db-query-matchers", "~> 0.9.0"
|
29
|
+
|
30
|
+
|
31
|
+
spec.add_dependency "rails", ">= 5.0", "< 5.2"
|
32
|
+
spec.add_dependency "relation_to_struct"
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pg_ha_migrations
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- jcoleman
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-11-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.15'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.15'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pg
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: db-query-matchers
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.9.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.9.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rails
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '5.0'
|
90
|
+
- - "<"
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '5.2'
|
93
|
+
type: :runtime
|
94
|
+
prerelease: false
|
95
|
+
version_requirements: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '5.0'
|
100
|
+
- - "<"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '5.2'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: relation_to_struct
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
type: :runtime
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
description: ''
|
118
|
+
email:
|
119
|
+
- code@getbraintree.com
|
120
|
+
executables: []
|
121
|
+
extensions: []
|
122
|
+
extra_rdoc_files: []
|
123
|
+
files:
|
124
|
+
- ".gitignore"
|
125
|
+
- ".rspec"
|
126
|
+
- ".ruby-version"
|
127
|
+
- ".travis.yml"
|
128
|
+
- CODE_OF_CONDUCT.md
|
129
|
+
- Gemfile
|
130
|
+
- LICENSE.txt
|
131
|
+
- README.md
|
132
|
+
- Rakefile
|
133
|
+
- bin/console
|
134
|
+
- bin/setup
|
135
|
+
- lib/pg_ha_migrations.rb
|
136
|
+
- lib/pg_ha_migrations/allowed_versions.rb
|
137
|
+
- lib/pg_ha_migrations/blocking_database_transactions.rb
|
138
|
+
- lib/pg_ha_migrations/blocking_database_transactions_reporter.rb
|
139
|
+
- lib/pg_ha_migrations/railtie.rb
|
140
|
+
- lib/pg_ha_migrations/safe_statements.rb
|
141
|
+
- lib/pg_ha_migrations/unsafe_statements.rb
|
142
|
+
- lib/pg_ha_migrations/version.rb
|
143
|
+
- lib/tasks/blocking_transactions.rake
|
144
|
+
- pg_ha_migrations.gemspec
|
145
|
+
homepage: ''
|
146
|
+
licenses:
|
147
|
+
- MIT
|
148
|
+
metadata: {}
|
149
|
+
post_install_message:
|
150
|
+
rdoc_options: []
|
151
|
+
require_paths:
|
152
|
+
- lib
|
153
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - ">="
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
159
|
+
requirements:
|
160
|
+
- - ">="
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
requirements: []
|
164
|
+
rubyforge_project:
|
165
|
+
rubygems_version: 2.7.8
|
166
|
+
signing_key:
|
167
|
+
specification_version: 4
|
168
|
+
summary: ''
|
169
|
+
test_files: []
|