pg_ha_migrations 1.7.0 → 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +5 -3
- data/Appraisals +6 -2
- data/Dockerfile +2 -2
- data/README.md +79 -2
- data/docker-compose.yml +1 -1
- data/gemfiles/rails_6.1.gemfile +1 -1
- data/gemfiles/rails_7.0.gemfile +1 -1
- data/gemfiles/rails_7.1.gemfile +7 -0
- data/lib/pg_ha_migrations/allowed_versions.rb +1 -1
- data/lib/pg_ha_migrations/blocking_database_transactions.rb +10 -5
- data/lib/pg_ha_migrations/hacks/add_index_on_only.rb +30 -0
- data/lib/pg_ha_migrations/hacks/disable_ddl_transaction.rb +0 -1
- data/lib/pg_ha_migrations/lock_mode.rb +100 -0
- data/lib/pg_ha_migrations/relation.rb +155 -0
- data/lib/pg_ha_migrations/safe_statements.rb +152 -51
- data/lib/pg_ha_migrations/unsafe_statements.rb +10 -0
- data/lib/pg_ha_migrations/version.rb +1 -1
- data/lib/pg_ha_migrations.rb +20 -0
- data/pg_ha_migrations.gemspec +2 -2
- metadata +10 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6858f02b9a874bbaf79865c789a490c9aa1140240537d0eded8f48486c6348f3
|
4
|
+
data.tar.gz: 733394b7f83821f71821816777765d982d1580761522e0751534d3e3c62f598b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 55f3f8e3730fc11183e71cbf6ba9d32dabf1c8b0ec532da6861b47d045cffb348184350831f564999ecee22b601931ab577ca20f8c4e831e0d4b776babe21a58
|
7
|
+
data.tar.gz: e260ebe93cafba7f3119c41bd2594a797293713f95d072378dbc5733f176d86ce8a93c8a24b53b6fbb9ec77756f4db9ab5fdb92f6080de6ac0fb56e158e5b5d3
|
data/.github/workflows/ci.yml
CHANGED
@@ -10,13 +10,15 @@ jobs:
|
|
10
10
|
- 13
|
11
11
|
- 14
|
12
12
|
- 15
|
13
|
+
- 16
|
13
14
|
ruby:
|
14
|
-
- 3.0
|
15
|
-
- 3.1
|
16
|
-
- 3.2
|
15
|
+
- "3.0"
|
16
|
+
- "3.1"
|
17
|
+
- "3.2"
|
17
18
|
gemfile:
|
18
19
|
- rails_6.1
|
19
20
|
- rails_7.0
|
21
|
+
- rails_7.1
|
20
22
|
name: PostgreSQL ${{ matrix.pg }} - Ruby ${{ matrix.ruby }} - ${{ matrix.gemfile }}
|
21
23
|
runs-on: ubuntu-latest
|
22
24
|
env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
|
data/Appraisals
CHANGED
data/Dockerfile
CHANGED
@@ -6,6 +6,6 @@ RUN apt-get update && apt-get install -y curl ca-certificates gnupg lsb-release
|
|
6
6
|
|
7
7
|
RUN curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/trusted.gpg.d/apt.postgresql.org.gpg >/dev/null
|
8
8
|
|
9
|
-
RUN echo "deb
|
9
|
+
RUN echo "deb https://apt-archive.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg-archive main" > /etc/apt/sources.list.d/pgdg.list
|
10
10
|
|
11
|
-
RUN apt update && apt-get install -y postgresql-$PG_MAJOR-partman
|
11
|
+
RUN apt update && apt-get install -y postgresql-$PG_MAJOR-partman=4.7.4-2.pgdg110+1
|
data/README.md
CHANGED
@@ -68,7 +68,7 @@ When `unsafe_*` migration methods support checks of this type you can bypass the
|
|
68
68
|
|
69
69
|
Similarly we believe the `force: true` option to ActiveRecord's `create_table` method is always unsafe, and therefore we disallow it even when calling `unsafe_create_table`. This option won't be enabled by default until 2.0, but you can opt-in by setting `config.allow_force_create_table = false` [in your configuration initializer](#configuration).
|
70
70
|
|
71
|
-
[Running multiple DDL statements inside a transaction acquires exclusive locks on all of the modified objects](https://medium.com/paypal-tech/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680#cc22). For that reason, this gem [disables DDL transactions](./lib/pg_ha_migrations.rb
|
71
|
+
[Running multiple DDL statements inside a transaction acquires exclusive locks on all of the modified objects](https://medium.com/paypal-tech/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680#cc22). For that reason, this gem [disables DDL transactions](./lib/pg_ha_migrations/hacks/disable_ddl_transaction.rb) by default. You can change this by resetting `ActiveRecord::Migration.disable_ddl_transaction` in your application.
|
72
72
|
|
73
73
|
The following functionality is currently unsupported:
|
74
74
|
|
@@ -76,6 +76,11 @@ The following functionality is currently unsupported:
|
|
76
76
|
- Generators
|
77
77
|
- schema.rb
|
78
78
|
|
79
|
+
Compatibility notes:
|
80
|
+
|
81
|
+
- While some features may work with other versions, this gem is currently tested against PostgreSQL 11+ and Partman 4.x
|
82
|
+
- There is a [bug](https://github.com/rails/rails/pull/41490) in early versions of Rails 6.1 when using `algorithm: :concurrently`. To add / remove indexes concurrently, please upgrade to at least Rails 6.1.4.
|
83
|
+
|
79
84
|
#### safe\_create\_table
|
80
85
|
|
81
86
|
Safely creates a new table.
|
@@ -166,6 +171,14 @@ Unsafely make a column not nullable.
|
|
166
171
|
unsafe_make_column_not_nullable :table, :column
|
167
172
|
```
|
168
173
|
|
174
|
+
#### safe\_add\_index\_on\_empty\_table
|
175
|
+
|
176
|
+
Safely add an index on a table with zero rows. This will raise an error if the table contains data.
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
safe_add_index_on_empty_table :table, :column
|
180
|
+
```
|
181
|
+
|
169
182
|
#### safe\_add\_concurrent\_index
|
170
183
|
|
171
184
|
Add an index concurrently.
|
@@ -188,6 +201,41 @@ Safely remove an index. Migrations that contain this statement must also include
|
|
188
201
|
safe_remove_concurrent_index :table, :name => :index_name
|
189
202
|
```
|
190
203
|
|
204
|
+
#### safe\_add\_concurrent\_partitioned\_index
|
205
|
+
|
206
|
+
Add an index to a natively partitioned table concurrently, as described in the [table partitioning docs](https://www.postgresql.org/docs/current/ddl-partitioning.html):
|
207
|
+
|
208
|
+
> To avoid long lock times, it is possible to use `CREATE INDEX ON ONLY` the partitioned table; such an index is marked invalid, and the partitions do not get the index applied automatically.
|
209
|
+
> The indexes on partitions can be created individually using `CONCURRENTLY`, and then attached to the index on the parent using `ALTER INDEX .. ATTACH PARTITION`.
|
210
|
+
> Once indexes for all partitions are attached to the parent index, the parent index is marked valid automatically.
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
# Assuming this table has partitions child1 and child2, the following indexes will be created:
|
214
|
+
# - index_partitioned_table_on_column
|
215
|
+
# - index_child1_on_column (attached to index_partitioned_table_on_column)
|
216
|
+
# - index_child2_on_column (attached to index_partitioned_table_on_column)
|
217
|
+
safe_add_concurrent_partitioned_index :partitioned_table, :column
|
218
|
+
```
|
219
|
+
|
220
|
+
Add a composite index using the `hash` index type with custom name for the parent index when the parent table contains sub-partitions.
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
# Assuming this table has partitions child1 and child2, and child1 has sub-partitions sub1 and sub2,
|
224
|
+
# the following indexes will be created:
|
225
|
+
# - custom_name_idx
|
226
|
+
# - index_child1_on_column1_column2 (attached to custom_name_idx)
|
227
|
+
# - index_sub1_on_column1_column2 (attached to index_child1_on_column1_column2)
|
228
|
+
# - index_sub2_on_column1_column2 (attached to index_child1_on_column1_column2)
|
229
|
+
# - index_child2_on_column1_column2 (attached to custom_name_idx)
|
230
|
+
safe_add_concurrent_partitioned_index :partitioned_table, [:column1, :column2], name: "custom_name_idx", using: :hash
|
231
|
+
```
|
232
|
+
|
233
|
+
Note:
|
234
|
+
|
235
|
+
This method runs multiple DDL statements non-transactionally.
|
236
|
+
Creating or attaching an index on a child table could fail.
|
237
|
+
In such cases an exception will be raised, and an `INVALID` index will be left on the parent table.
|
238
|
+
|
191
239
|
#### safe\_add\_unvalidated\_check\_constraint
|
192
240
|
|
193
241
|
Safely add a `CHECK` constraint. The constraint will not be immediately validated on existing rows to avoid a full table scan while holding an exclusive lock. After adding the constraint, you'll need to use `safe_validate_check_constraint` to validate existing rows.
|
@@ -387,7 +435,7 @@ safe_partman_reapply_privileges :table
|
|
387
435
|
|
388
436
|
#### safely\_acquire\_lock\_for\_table
|
389
437
|
|
390
|
-
Safely acquire
|
438
|
+
Safely acquire an access exclusive lock for a table.
|
391
439
|
|
392
440
|
```ruby
|
393
441
|
safely_acquire_lock_for_table(:table) do
|
@@ -395,6 +443,19 @@ safely_acquire_lock_for_table(:table) do
|
|
395
443
|
end
|
396
444
|
```
|
397
445
|
|
446
|
+
Safely acquire a lock for a table in a different mode.
|
447
|
+
|
448
|
+
```ruby
|
449
|
+
safely_acquire_lock_for_table(:table, mode: :share) do
|
450
|
+
...
|
451
|
+
end
|
452
|
+
```
|
453
|
+
|
454
|
+
Note:
|
455
|
+
|
456
|
+
We enforce that only one table (or a table and its partitions) can be locked at a time.
|
457
|
+
Attempting to acquire a nested lock on a different table will result in an error.
|
458
|
+
|
398
459
|
#### adjust\_lock\_timeout
|
399
460
|
|
400
461
|
Adjust lock timeout.
|
@@ -423,6 +484,22 @@ Set maintenance work mem.
|
|
423
484
|
safe_set_maintenance_work_mem_gb 1
|
424
485
|
```
|
425
486
|
|
487
|
+
#### ensure\_small\_table!
|
488
|
+
|
489
|
+
Ensure a table on disk is below the default threshold (10 megabytes).
|
490
|
+
This will raise an error if the table is too large.
|
491
|
+
|
492
|
+
```ruby
|
493
|
+
ensure_small_table! :table
|
494
|
+
```
|
495
|
+
|
496
|
+
Ensure a table on disk is below a custom threshold and is empty.
|
497
|
+
This will raise an error if the table is too large and/or contains data.
|
498
|
+
|
499
|
+
```ruby
|
500
|
+
ensure_small_table! :table, empty: true, threshold: 100.megabytes
|
501
|
+
```
|
502
|
+
|
426
503
|
### Configuration
|
427
504
|
|
428
505
|
The gem can be configured in an initializer.
|
data/docker-compose.yml
CHANGED
data/gemfiles/rails_6.1.gemfile
CHANGED
data/gemfiles/rails_7.0.gemfile
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require "active_record/migration/compatibility"
|
2
2
|
|
3
3
|
module PgHaMigrations::AllowedVersions
|
4
|
-
ALLOWED_VERSIONS = [4.2, 5.0, 5.1, 5.2, 6.0, 6.1, 7.0].map do |v|
|
4
|
+
ALLOWED_VERSIONS = [4.2, 5.0, 5.1, 5.2, 6.0, 6.1, 7.0, 7.1].map do |v|
|
5
5
|
begin
|
6
6
|
ActiveRecord::Migration[v]
|
7
7
|
rescue ArgumentError
|
@@ -1,13 +1,18 @@
|
|
1
1
|
module PgHaMigrations
|
2
2
|
class BlockingDatabaseTransactions
|
3
3
|
LongRunningTransaction = Struct.new(:database, :current_query, :state, :transaction_age, :tables_with_locks) do
|
4
|
+
def initialize(*args)
|
5
|
+
super
|
6
|
+
|
7
|
+
self.tables_with_locks = tables_with_locks.map { |args| Table.new(*args) }.select(&:present?)
|
8
|
+
end
|
9
|
+
|
4
10
|
def description
|
5
|
-
locked_tables = tables_with_locks.compact
|
6
11
|
[
|
7
12
|
database,
|
8
|
-
|
13
|
+
tables_with_locks.size > 0 ? "tables (#{tables_with_locks.map(&:fully_qualified_name).join(', ')})" : nil,
|
9
14
|
"#{idle? ? "currently idle " : ""}transaction open for #{transaction_age}",
|
10
|
-
"#{idle? ? "last " : ""}query: #{current_query}"
|
15
|
+
"#{idle? ? "last " : ""}query: #{current_query}",
|
11
16
|
].compact.join(" | ")
|
12
17
|
end
|
13
18
|
|
@@ -43,7 +48,7 @@ module PgHaMigrations
|
|
43
48
|
psa.#{query_column} as current_query,
|
44
49
|
psa.state,
|
45
50
|
clock_timestamp() - psa.xact_start AS transaction_age,
|
46
|
-
array_agg(distinct c.relname) AS tables_with_locks
|
51
|
+
array_agg(distinct array[c.relname, ns.nspname, l.mode]) AS tables_with_locks
|
47
52
|
FROM pg_stat_activity psa -- Cluster wide
|
48
53
|
LEFT JOIN pg_locks l ON (psa.#{pid_column} = l.pid) -- Cluster wide
|
49
54
|
LEFT JOIN pg_class c ON ( -- Database wide
|
@@ -56,7 +61,7 @@ module PgHaMigrations
|
|
56
61
|
l.locktype != 'relation'
|
57
62
|
OR (
|
58
63
|
ns.nspname != 'pg_catalog'
|
59
|
-
AND c.relkind
|
64
|
+
AND c.relkind IN ('r', 'p') -- 'r' is a standard table; 'p' is a partition parent
|
60
65
|
)
|
61
66
|
)
|
62
67
|
AND psa.xact_start < clock_timestamp() - ?::interval
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "active_record/connection_adapters/postgresql_adapter"
|
2
|
+
require "active_record/connection_adapters/postgresql/schema_creation"
|
3
|
+
|
4
|
+
module PgHaMigrations
|
5
|
+
module ActiveRecordHacks
|
6
|
+
module IndexAlgorithms
|
7
|
+
def index_algorithms
|
8
|
+
super.merge(only: "ONLY")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module CreateIndexDefinition
|
13
|
+
def visit_CreateIndexDefinition(o)
|
14
|
+
if o.algorithm == "ONLY"
|
15
|
+
o.algorithm = nil
|
16
|
+
|
17
|
+
quoted_index = quote_column_name(o.index.name)
|
18
|
+
quoted_table = quote_table_name(o.index.table)
|
19
|
+
|
20
|
+
super.sub("#{quoted_index} ON #{quoted_table}", "#{quoted_index} ON ONLY #{quoted_table}")
|
21
|
+
else
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(PgHaMigrations::ActiveRecordHacks::IndexAlgorithms)
|
30
|
+
ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaCreation.prepend(PgHaMigrations::ActiveRecordHacks::CreateIndexDefinition)
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module PgHaMigrations
|
2
|
+
class LockMode
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
MODE_CONFLICTS = ActiveSupport::OrderedHash.new
|
6
|
+
|
7
|
+
MODE_CONFLICTS[:access_share] = %i[
|
8
|
+
access_exclusive
|
9
|
+
]
|
10
|
+
|
11
|
+
MODE_CONFLICTS[:row_share] = %i[
|
12
|
+
exclusive
|
13
|
+
access_exclusive
|
14
|
+
]
|
15
|
+
|
16
|
+
MODE_CONFLICTS[:row_exclusive] = %i[
|
17
|
+
share
|
18
|
+
share_row_exclusive
|
19
|
+
exclusive
|
20
|
+
access_exclusive
|
21
|
+
]
|
22
|
+
|
23
|
+
MODE_CONFLICTS[:share_update_exclusive] = %i[
|
24
|
+
share_update_exclusive
|
25
|
+
share
|
26
|
+
share_row_exclusive
|
27
|
+
exclusive
|
28
|
+
access_exclusive
|
29
|
+
]
|
30
|
+
|
31
|
+
MODE_CONFLICTS[:share] = %i[
|
32
|
+
row_exclusive
|
33
|
+
share_update_exclusive
|
34
|
+
share_row_exclusive
|
35
|
+
exclusive
|
36
|
+
access_exclusive
|
37
|
+
]
|
38
|
+
|
39
|
+
MODE_CONFLICTS[:share_row_exclusive] = %i[
|
40
|
+
row_exclusive
|
41
|
+
share_update_exclusive
|
42
|
+
share
|
43
|
+
share_row_exclusive
|
44
|
+
exclusive
|
45
|
+
access_exclusive
|
46
|
+
]
|
47
|
+
|
48
|
+
MODE_CONFLICTS[:exclusive] = %i[
|
49
|
+
row_share
|
50
|
+
row_exclusive
|
51
|
+
share_update_exclusive
|
52
|
+
share
|
53
|
+
share_row_exclusive
|
54
|
+
exclusive
|
55
|
+
access_exclusive
|
56
|
+
]
|
57
|
+
|
58
|
+
MODE_CONFLICTS[:access_exclusive] = %i[
|
59
|
+
access_share
|
60
|
+
row_share
|
61
|
+
row_exclusive
|
62
|
+
share_update_exclusive
|
63
|
+
share
|
64
|
+
share_row_exclusive
|
65
|
+
exclusive
|
66
|
+
access_exclusive
|
67
|
+
]
|
68
|
+
|
69
|
+
attr_reader :mode
|
70
|
+
|
71
|
+
delegate :to_s, to: :mode
|
72
|
+
|
73
|
+
def initialize(mode)
|
74
|
+
@mode = mode
|
75
|
+
.to_s
|
76
|
+
.underscore
|
77
|
+
.delete_suffix("_lock")
|
78
|
+
.to_sym
|
79
|
+
|
80
|
+
if !MODE_CONFLICTS.keys.include?(@mode)
|
81
|
+
raise ArgumentError, "Unrecognized lock mode #{@mode.inspect}. Valid modes: #{MODE_CONFLICTS.keys}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_sql
|
86
|
+
mode
|
87
|
+
.to_s
|
88
|
+
.upcase
|
89
|
+
.gsub("_", " ")
|
90
|
+
end
|
91
|
+
|
92
|
+
def <=>(other)
|
93
|
+
MODE_CONFLICTS.keys.index(mode) <=> MODE_CONFLICTS.keys.index(other.mode)
|
94
|
+
end
|
95
|
+
|
96
|
+
def conflicts_with?(other)
|
97
|
+
MODE_CONFLICTS[mode].include?(other.mode)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module PgHaMigrations
|
2
|
+
Relation = Struct.new(:name, :schema, :mode) do
|
3
|
+
def self.connection
|
4
|
+
ActiveRecord::Base.connection
|
5
|
+
end
|
6
|
+
|
7
|
+
delegate :inspect, to: :name
|
8
|
+
delegate :connection, to: :class
|
9
|
+
|
10
|
+
def initialize(name, schema, mode=nil)
|
11
|
+
super(name, schema)
|
12
|
+
|
13
|
+
self.mode = LockMode.new(mode) if mode.present?
|
14
|
+
end
|
15
|
+
|
16
|
+
def conflicts_with?(other)
|
17
|
+
self == other && (
|
18
|
+
mode.nil? || other.mode.nil? || mode.conflicts_with?(other.mode)
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def fully_qualified_name
|
23
|
+
@fully_qualified_name ||= [
|
24
|
+
PG::Connection.quote_ident(schema),
|
25
|
+
PG::Connection.quote_ident(name),
|
26
|
+
].join(".")
|
27
|
+
end
|
28
|
+
|
29
|
+
def present?
|
30
|
+
name.present? && schema.present?
|
31
|
+
end
|
32
|
+
|
33
|
+
def ==(other)
|
34
|
+
other.is_a?(Relation) && name == other.name && schema == other.schema
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Table < Relation
|
39
|
+
def self.from_table_name(table, mode=nil)
|
40
|
+
pg_name = ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(table.to_s)
|
41
|
+
|
42
|
+
schema_conditional = if pg_name.schema
|
43
|
+
"#{connection.quote(pg_name.schema)}"
|
44
|
+
else
|
45
|
+
"ANY (current_schemas(false))"
|
46
|
+
end
|
47
|
+
|
48
|
+
schema = connection.select_value(<<~SQL)
|
49
|
+
SELECT schemaname
|
50
|
+
FROM pg_tables
|
51
|
+
WHERE tablename = #{connection.quote(pg_name.identifier)} AND schemaname = #{schema_conditional}
|
52
|
+
ORDER BY array_position(current_schemas(false), schemaname)
|
53
|
+
LIMIT 1
|
54
|
+
SQL
|
55
|
+
|
56
|
+
raise UndefinedTableError, "Table #{pg_name.quoted} does not exist#{" in search path" unless pg_name.schema}" unless schema.present?
|
57
|
+
|
58
|
+
new(pg_name.identifier, schema, mode)
|
59
|
+
end
|
60
|
+
|
61
|
+
def natively_partitioned?
|
62
|
+
return @natively_partitioned if defined?(@natively_partitioned)
|
63
|
+
|
64
|
+
@natively_partitioned = !!connection.select_value(<<~SQL)
|
65
|
+
SELECT true
|
66
|
+
FROM pg_partitioned_table, pg_class, pg_namespace
|
67
|
+
WHERE pg_class.oid = pg_partitioned_table.partrelid
|
68
|
+
AND pg_class.relnamespace = pg_namespace.oid
|
69
|
+
AND pg_class.relname = #{connection.quote(name)}
|
70
|
+
AND pg_namespace.nspname = #{connection.quote(schema)}
|
71
|
+
SQL
|
72
|
+
end
|
73
|
+
|
74
|
+
def partitions(include_sub_partitions: false, include_self: false)
|
75
|
+
tables = connection.structs_from_sql(self.class, <<~SQL)
|
76
|
+
SELECT child.relname AS name, child_ns.nspname AS schema, NULLIF('#{mode}', '') AS mode
|
77
|
+
FROM pg_inherits
|
78
|
+
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
|
79
|
+
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
|
80
|
+
JOIN pg_namespace parent_ns ON parent.relnamespace = parent_ns.oid
|
81
|
+
JOIN pg_namespace child_ns ON child.relnamespace = child_ns.oid
|
82
|
+
WHERE parent.relname = #{connection.quote(name)}
|
83
|
+
AND parent_ns.nspname = #{connection.quote(schema)}
|
84
|
+
ORDER BY child.oid -- Ensure consistent ordering for tests
|
85
|
+
SQL
|
86
|
+
|
87
|
+
if include_sub_partitions
|
88
|
+
sub_partitions = tables.each_with_object([]) do |table, arr|
|
89
|
+
arr.concat(table.partitions(include_sub_partitions: true))
|
90
|
+
end
|
91
|
+
|
92
|
+
tables.concat(sub_partitions)
|
93
|
+
end
|
94
|
+
|
95
|
+
tables.prepend(self) if include_self
|
96
|
+
|
97
|
+
tables
|
98
|
+
end
|
99
|
+
|
100
|
+
def has_rows?
|
101
|
+
connection.select_value("SELECT EXISTS (SELECT 1 FROM #{fully_qualified_name} LIMIT 1)")
|
102
|
+
end
|
103
|
+
|
104
|
+
def total_bytes
|
105
|
+
connection.select_value(<<~SQL)
|
106
|
+
SELECT pg_total_relation_size(pg_class.oid)
|
107
|
+
FROM pg_class, pg_namespace
|
108
|
+
WHERE pg_class.relname = #{connection.quote(name)}
|
109
|
+
AND pg_namespace.nspname = #{connection.quote(schema)}
|
110
|
+
SQL
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class Index < Relation
|
115
|
+
MAX_NAME_SIZE = 63 # bytes
|
116
|
+
|
117
|
+
def self.from_table_and_columns(table, columns)
|
118
|
+
name = connection.index_name(table.name, columns)
|
119
|
+
|
120
|
+
# modified from https://github.com/rails/rails/pull/47753
|
121
|
+
if name.bytesize > MAX_NAME_SIZE
|
122
|
+
hashed_identifier = "_#{OpenSSL::Digest::SHA256.hexdigest(name).first(10)}"
|
123
|
+
description = name.sub("index_#{table.name}_on", "idx_on")
|
124
|
+
|
125
|
+
short_limit = MAX_NAME_SIZE - hashed_identifier.bytesize
|
126
|
+
short_description = description.mb_chars.limit(short_limit).to_s
|
127
|
+
|
128
|
+
name = "#{short_description}#{hashed_identifier}"
|
129
|
+
end
|
130
|
+
|
131
|
+
new(name, table)
|
132
|
+
end
|
133
|
+
|
134
|
+
attr_accessor :table
|
135
|
+
|
136
|
+
def initialize(name, table)
|
137
|
+
super(name, table.schema)
|
138
|
+
|
139
|
+
self.table = table
|
140
|
+
|
141
|
+
connection.send(:validate_index_length!, table.name, name)
|
142
|
+
end
|
143
|
+
|
144
|
+
def valid?
|
145
|
+
!!connection.select_value(<<~SQL)
|
146
|
+
SELECT pg_index.indisvalid
|
147
|
+
FROM pg_index, pg_class, pg_namespace
|
148
|
+
WHERE pg_class.oid = pg_index.indexrelid
|
149
|
+
AND pg_class.relnamespace = pg_namespace.oid
|
150
|
+
AND pg_namespace.nspname = #{connection.quote(schema)}
|
151
|
+
AND pg_class.relname = #{connection.quote(name)}
|
152
|
+
SQL
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -1,14 +1,4 @@
|
|
1
1
|
module PgHaMigrations::SafeStatements
|
2
|
-
PARTITION_TYPES = %i[range list hash]
|
3
|
-
|
4
|
-
PARTMAN_UPDATE_CONFIG_OPTIONS = %i[
|
5
|
-
infinite_time_partitions
|
6
|
-
inherit_privileges
|
7
|
-
premake
|
8
|
-
retention
|
9
|
-
retention_keep_table
|
10
|
-
]
|
11
|
-
|
12
2
|
def safe_added_columns_without_default_value
|
13
3
|
@safe_added_columns_without_default_value ||= []
|
14
4
|
end
|
@@ -154,6 +144,22 @@ module PgHaMigrations::SafeStatements
|
|
154
144
|
end
|
155
145
|
end
|
156
146
|
|
147
|
+
def safe_add_index_on_empty_table(table, columns, options={})
|
148
|
+
if options[:algorithm] == :concurrently
|
149
|
+
raise ArgumentError, "Cannot call safe_add_index_on_empty_table with :algorithm => :concurrently"
|
150
|
+
end
|
151
|
+
|
152
|
+
# Avoids taking out an unnecessary SHARE lock if the table does have data
|
153
|
+
ensure_small_table!(table, empty: true)
|
154
|
+
|
155
|
+
safely_acquire_lock_for_table(table, mode: :share) do
|
156
|
+
# Ensure data wasn't written in the split second after the first check
|
157
|
+
ensure_small_table!(table, empty: true)
|
158
|
+
|
159
|
+
unsafe_add_index(table, columns, **options)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
157
163
|
def safe_add_concurrent_index(table, columns, options={})
|
158
164
|
unsafe_add_index(table, columns, **options.merge(:algorithm => :concurrently))
|
159
165
|
end
|
@@ -170,6 +176,90 @@ module PgHaMigrations::SafeStatements
|
|
170
176
|
unsafe_remove_index(table, **options.merge(:algorithm => :concurrently))
|
171
177
|
end
|
172
178
|
|
179
|
+
def safe_add_concurrent_partitioned_index(
|
180
|
+
table,
|
181
|
+
columns,
|
182
|
+
name: nil,
|
183
|
+
if_not_exists: nil,
|
184
|
+
using: nil,
|
185
|
+
unique: nil,
|
186
|
+
where: nil,
|
187
|
+
comment: nil
|
188
|
+
)
|
189
|
+
|
190
|
+
if ActiveRecord::Base.connection.postgresql_version < 11_00_00
|
191
|
+
raise PgHaMigrations::InvalidMigrationError, "Concurrent partitioned index creation not supported on Postgres databases before version 11"
|
192
|
+
end
|
193
|
+
|
194
|
+
parent_table = PgHaMigrations::Table.from_table_name(table)
|
195
|
+
|
196
|
+
raise PgHaMigrations::InvalidMigrationError, "Table #{parent_table.inspect} is not a partitioned table" unless parent_table.natively_partitioned?
|
197
|
+
|
198
|
+
parent_index = if name.present?
|
199
|
+
PgHaMigrations::Index.new(name, parent_table)
|
200
|
+
else
|
201
|
+
PgHaMigrations::Index.from_table_and_columns(parent_table, columns)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Short-circuit when if_not_exists: true and index already valid
|
205
|
+
return if if_not_exists && parent_index.valid?
|
206
|
+
|
207
|
+
child_indexes = parent_table.partitions.map do |child_table|
|
208
|
+
PgHaMigrations::Index.from_table_and_columns(child_table, columns)
|
209
|
+
end
|
210
|
+
|
211
|
+
# TODO: take out ShareLock after issue #39 is implemented
|
212
|
+
safely_acquire_lock_for_table(parent_table.fully_qualified_name) do
|
213
|
+
# CREATE INDEX ON ONLY parent_table
|
214
|
+
unsafe_add_index(
|
215
|
+
parent_table.fully_qualified_name,
|
216
|
+
columns,
|
217
|
+
name: parent_index.name,
|
218
|
+
if_not_exists: if_not_exists,
|
219
|
+
using: using,
|
220
|
+
unique: unique,
|
221
|
+
where: where,
|
222
|
+
comment: comment,
|
223
|
+
algorithm: :only, # see lib/pg_ha_migrations/hacks/add_index_on_only.rb
|
224
|
+
)
|
225
|
+
end
|
226
|
+
|
227
|
+
child_indexes.each do |child_index|
|
228
|
+
add_index_method = if child_index.table.natively_partitioned?
|
229
|
+
:safe_add_concurrent_partitioned_index
|
230
|
+
else
|
231
|
+
:safe_add_concurrent_index
|
232
|
+
end
|
233
|
+
|
234
|
+
send(
|
235
|
+
add_index_method,
|
236
|
+
child_index.table.fully_qualified_name,
|
237
|
+
columns,
|
238
|
+
name: child_index.name,
|
239
|
+
if_not_exists: if_not_exists,
|
240
|
+
using: using,
|
241
|
+
unique: unique,
|
242
|
+
where: where,
|
243
|
+
)
|
244
|
+
end
|
245
|
+
|
246
|
+
# Avoid taking out an unnecessary lock if there are no child tables to attach
|
247
|
+
if child_indexes.present?
|
248
|
+
safely_acquire_lock_for_table(parent_table.fully_qualified_name) do
|
249
|
+
child_indexes.each do |child_index|
|
250
|
+
say_with_time "Attaching index #{child_index.inspect} to #{parent_index.inspect}" do
|
251
|
+
connection.execute(<<~SQL)
|
252
|
+
ALTER INDEX #{parent_index.fully_qualified_name}
|
253
|
+
ATTACH PARTITION #{child_index.fully_qualified_name}
|
254
|
+
SQL
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
raise PgHaMigrations::InvalidMigrationError, "Unexpected state. Parent index #{parent_index.inspect} is invalid" unless parent_index.valid?
|
261
|
+
end
|
262
|
+
|
173
263
|
def safe_set_maintenance_work_mem_gb(gigabytes)
|
174
264
|
unsafe_execute("SET maintenance_work_mem = '#{PG::Connection.escape_string(gigabytes.to_s)} GB'")
|
175
265
|
end
|
@@ -223,8 +313,8 @@ module PgHaMigrations::SafeStatements
|
|
223
313
|
def safe_create_partitioned_table(table, partition_key:, type:, infer_primary_key: nil, **options, &block)
|
224
314
|
raise ArgumentError, "Expected <partition_key> to be present" unless partition_key.present?
|
225
315
|
|
226
|
-
unless PARTITION_TYPES.include?(type)
|
227
|
-
raise ArgumentError, "Expected <type> to be symbol in #{PARTITION_TYPES} but received #{type.inspect}"
|
316
|
+
unless PgHaMigrations::PARTITION_TYPES.include?(type)
|
317
|
+
raise ArgumentError, "Expected <type> to be symbol in #{PgHaMigrations::PARTITION_TYPES} but received #{type.inspect}"
|
228
318
|
end
|
229
319
|
|
230
320
|
if ActiveRecord::Base.connection.postgresql_version < 10_00_00
|
@@ -351,7 +441,7 @@ module PgHaMigrations::SafeStatements
|
|
351
441
|
retention_keep_table: retention_keep_table,
|
352
442
|
}.compact
|
353
443
|
|
354
|
-
unsafe_partman_update_config(
|
444
|
+
unsafe_partman_update_config(table, **update_config_options)
|
355
445
|
end
|
356
446
|
|
357
447
|
def safe_partman_update_config(table, **options)
|
@@ -363,7 +453,7 @@ module PgHaMigrations::SafeStatements
|
|
363
453
|
end
|
364
454
|
|
365
455
|
def unsafe_partman_update_config(table, **options)
|
366
|
-
invalid_options = options.keys - PARTMAN_UPDATE_CONFIG_OPTIONS
|
456
|
+
invalid_options = options.keys - PgHaMigrations::PARTMAN_UPDATE_CONFIG_OPTIONS
|
367
457
|
|
368
458
|
raise ArgumentError, "Unrecognized argument(s): #{invalid_options}" unless invalid_options.empty?
|
369
459
|
|
@@ -402,38 +492,13 @@ module PgHaMigrations::SafeStatements
|
|
402
492
|
end
|
403
493
|
|
404
494
|
def _fully_qualified_table_name_for_partman(table)
|
405
|
-
|
406
|
-
|
407
|
-
raise PgHaMigrations::InvalidMigrationError, "Expected table to be in the format <table> or <schema>.<table> but received #{table}" if identifiers.size > 2
|
408
|
-
|
409
|
-
identifiers.each { |identifier| _validate_partman_identifier(identifier) }
|
410
|
-
|
411
|
-
schema_conditional = if identifiers.size > 1
|
412
|
-
"'#{identifiers.first}'"
|
413
|
-
else
|
414
|
-
"ANY (current_schemas(false))"
|
415
|
-
end
|
495
|
+
table = PgHaMigrations::Table.from_table_name(table)
|
416
496
|
|
417
|
-
schema
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
LIMIT 1
|
423
|
-
SQL
|
424
|
-
|
425
|
-
raise PgHaMigrations::InvalidMigrationError, "Could not find table #{table}" unless schema.present?
|
426
|
-
|
427
|
-
_validate_partman_identifier(schema)
|
428
|
-
|
429
|
-
# Quoting is unneeded since _validate_partman_identifier ensures the schema / table use standard naming conventions
|
430
|
-
"#{schema}.#{identifiers.last}"
|
431
|
-
end
|
432
|
-
|
433
|
-
def _validate_partman_identifier(identifier)
|
434
|
-
if identifier.to_s !~ /^[a-z_][a-z_\d]*$/
|
435
|
-
raise PgHaMigrations::InvalidMigrationError, "Partman requires schema / table names to be lowercase with underscores"
|
436
|
-
end
|
497
|
+
[table.schema, table.name].each do |identifier|
|
498
|
+
if identifier.to_s !~ /^[a-z_][a-z_\d]*$/
|
499
|
+
raise PgHaMigrations::InvalidMigrationError, "Partman requires schema / table names to be lowercase with underscores"
|
500
|
+
end
|
501
|
+
end.join(".")
|
437
502
|
end
|
438
503
|
|
439
504
|
def _per_migration_caller
|
@@ -463,17 +528,39 @@ module PgHaMigrations::SafeStatements
|
|
463
528
|
super(conn, direction)
|
464
529
|
end
|
465
530
|
|
466
|
-
def safely_acquire_lock_for_table(table, &block)
|
531
|
+
def safely_acquire_lock_for_table(table, mode: :access_exclusive, &block)
|
532
|
+
nested_target_table = Thread.current[__method__]
|
533
|
+
|
467
534
|
_check_postgres_adapter!
|
468
|
-
|
469
|
-
|
535
|
+
|
536
|
+
target_table = PgHaMigrations::Table.from_table_name(table, mode)
|
537
|
+
|
538
|
+
if nested_target_table
|
539
|
+
if nested_target_table != target_table
|
540
|
+
raise PgHaMigrations::InvalidMigrationError, "Nested lock detected! Cannot acquire lock on #{target_table.fully_qualified_name} while #{nested_target_table.fully_qualified_name} is locked."
|
541
|
+
elsif nested_target_table.mode < target_table.mode
|
542
|
+
raise PgHaMigrations::InvalidMigrationError, "Lock escalation detected! Cannot change lock level from :#{nested_target_table.mode} to :#{target_table.mode} for #{target_table.fully_qualified_name}."
|
543
|
+
end
|
544
|
+
else
|
545
|
+
Thread.current[__method__] = target_table
|
546
|
+
end
|
547
|
+
|
548
|
+
# Locking a partitioned table will also lock child tables (including sub-partitions),
|
549
|
+
# so we need to check for blocking queries on those tables as well
|
550
|
+
target_tables = target_table.partitions(include_sub_partitions: true, include_self: true)
|
470
551
|
|
471
552
|
successfully_acquired_lock = false
|
472
553
|
|
473
554
|
until successfully_acquired_lock
|
474
555
|
while (
|
475
556
|
blocking_transactions = PgHaMigrations::BlockingDatabaseTransactions.find_blocking_transactions("#{PgHaMigrations::LOCK_TIMEOUT_SECONDS} seconds")
|
476
|
-
blocking_transactions.any?
|
557
|
+
blocking_transactions.any? do |query|
|
558
|
+
query.tables_with_locks.any? do |locked_table|
|
559
|
+
target_tables.any? do |target_table|
|
560
|
+
target_table.conflicts_with?(locked_table)
|
561
|
+
end
|
562
|
+
end
|
563
|
+
end
|
477
564
|
)
|
478
565
|
say "Waiting on blocking transactions:"
|
479
566
|
blocking_transactions.each do |blocking_transaction|
|
@@ -486,13 +573,13 @@ module PgHaMigrations::SafeStatements
|
|
486
573
|
adjust_timeout_method = connection.postgresql_version >= 9_03_00 ? :adjust_lock_timeout : :adjust_statement_timeout
|
487
574
|
begin
|
488
575
|
method(adjust_timeout_method).call(PgHaMigrations::LOCK_TIMEOUT_SECONDS) do
|
489
|
-
connection.execute("LOCK #{
|
576
|
+
connection.execute("LOCK #{target_table.fully_qualified_name} IN #{target_table.mode.to_sql} MODE;")
|
490
577
|
end
|
491
578
|
successfully_acquired_lock = true
|
492
579
|
rescue ActiveRecord::StatementInvalid => e
|
493
580
|
if e.message =~ /PG::LockNotAvailable.+ lock timeout/ || e.message =~ /PG::QueryCanceled.+ statement timeout/
|
494
581
|
sleep_seconds = PgHaMigrations::LOCK_FAILURE_RETRY_DELAY_MULTLIPLIER * PgHaMigrations::LOCK_TIMEOUT_SECONDS
|
495
|
-
say "Timed out trying to acquire
|
582
|
+
say "Timed out trying to acquire #{target_table.mode.to_sql} lock on the #{target_table.fully_qualified_name} table."
|
496
583
|
say "Sleeping for #{sleep_seconds}s to allow potentially queued up queries to finish before continuing."
|
497
584
|
sleep(sleep_seconds)
|
498
585
|
|
@@ -507,6 +594,8 @@ module PgHaMigrations::SafeStatements
|
|
507
594
|
end
|
508
595
|
end
|
509
596
|
end
|
597
|
+
ensure
|
598
|
+
Thread.current[__method__] = nil unless nested_target_table
|
510
599
|
end
|
511
600
|
|
512
601
|
def adjust_lock_timeout(timeout_seconds = PgHaMigrations::LOCK_TIMEOUT_SECONDS, &block)
|
@@ -548,4 +637,16 @@ module PgHaMigrations::SafeStatements
|
|
548
637
|
end
|
549
638
|
end
|
550
639
|
end
|
640
|
+
|
641
|
+
def ensure_small_table!(table, empty: false, threshold: PgHaMigrations::SMALL_TABLE_THRESHOLD_BYTES)
|
642
|
+
table = PgHaMigrations::Table.from_table_name(table)
|
643
|
+
|
644
|
+
if empty && table.has_rows?
|
645
|
+
raise PgHaMigrations::InvalidMigrationError, "Table #{table.inspect} has rows"
|
646
|
+
end
|
647
|
+
|
648
|
+
if table.total_bytes > threshold
|
649
|
+
raise PgHaMigrations::InvalidMigrationError, "Table #{table.inspect} is larger than #{threshold} bytes"
|
650
|
+
end
|
651
|
+
end
|
551
652
|
end
|
@@ -80,6 +80,16 @@ module PgHaMigrations::UnsafeStatements
|
|
80
80
|
raise PgHaMigrations::InvalidMigrationError, "ActiveRecord drops the :opclass option when supplying a string containing an expression or list of columns; instead either supply an array of columns or include the opclass in the string for each column"
|
81
81
|
end
|
82
82
|
|
83
|
+
validated_table = PgHaMigrations::Table.from_table_name(table)
|
84
|
+
|
85
|
+
validated_index = if options[:name]
|
86
|
+
PgHaMigrations::Index.new(options[:name], validated_table)
|
87
|
+
else
|
88
|
+
PgHaMigrations::Index.from_table_and_columns(validated_table, column_names)
|
89
|
+
end
|
90
|
+
|
91
|
+
options[:name] = validated_index.name
|
92
|
+
|
83
93
|
execute_ancestor_statement(:add_index, table, column_names, **options)
|
84
94
|
end
|
85
95
|
|
data/lib/pg_ha_migrations.rb
CHANGED
@@ -2,6 +2,8 @@ require "pg_ha_migrations/version"
|
|
2
2
|
require "rails"
|
3
3
|
require "active_record"
|
4
4
|
require "active_record/migration"
|
5
|
+
require "active_record/connection_adapters/postgresql/utils"
|
6
|
+
require "active_support/core_ext/numeric/bytes"
|
5
7
|
require "relation_to_struct"
|
6
8
|
require "ruby2_keywords"
|
7
9
|
|
@@ -30,6 +32,17 @@ module PgHaMigrations
|
|
30
32
|
|
31
33
|
LOCK_TIMEOUT_SECONDS = 5
|
32
34
|
LOCK_FAILURE_RETRY_DELAY_MULTLIPLIER = 5
|
35
|
+
SMALL_TABLE_THRESHOLD_BYTES = 10.megabytes
|
36
|
+
|
37
|
+
PARTITION_TYPES = %i[range list hash]
|
38
|
+
|
39
|
+
PARTMAN_UPDATE_CONFIG_OPTIONS = %i[
|
40
|
+
infinite_time_partitions
|
41
|
+
inherit_privileges
|
42
|
+
premake
|
43
|
+
retention
|
44
|
+
retention_keep_table
|
45
|
+
]
|
33
46
|
|
34
47
|
# Safe versus unsafe in this context specifically means the following:
|
35
48
|
# - Safe operations will not block for long periods of time.
|
@@ -52,11 +65,17 @@ module PgHaMigrations
|
|
52
65
|
|
53
66
|
# This gem only supports the PostgreSQL adapter at this time.
|
54
67
|
UnsupportedAdapter = Class.new(StandardError)
|
68
|
+
|
69
|
+
# Some methods need to inspect the attributes of a table. In such cases,
|
70
|
+
# this error will be raised if the table does not exist
|
71
|
+
UndefinedTableError = Class.new(StandardError)
|
55
72
|
end
|
56
73
|
|
74
|
+
require "pg_ha_migrations/relation"
|
57
75
|
require "pg_ha_migrations/blocking_database_transactions"
|
58
76
|
require "pg_ha_migrations/blocking_database_transactions_reporter"
|
59
77
|
require "pg_ha_migrations/partman_config"
|
78
|
+
require "pg_ha_migrations/lock_mode"
|
60
79
|
require "pg_ha_migrations/unsafe_statements"
|
61
80
|
require "pg_ha_migrations/safe_statements"
|
62
81
|
require "pg_ha_migrations/dependent_objects_checks"
|
@@ -64,6 +83,7 @@ require "pg_ha_migrations/allowed_versions"
|
|
64
83
|
require "pg_ha_migrations/railtie"
|
65
84
|
require "pg_ha_migrations/hacks/disable_ddl_transaction"
|
66
85
|
require "pg_ha_migrations/hacks/cleanup_unnecessary_output"
|
86
|
+
require "pg_ha_migrations/hacks/add_index_on_only"
|
67
87
|
|
68
88
|
module PgHaMigrations::AutoIncluder
|
69
89
|
def inherited(klass)
|
data/pg_ha_migrations.gemspec
CHANGED
@@ -32,12 +32,12 @@ Gem::Specification.new do |spec|
|
|
32
32
|
spec.add_development_dependency "rake", ">= 12.3.3"
|
33
33
|
spec.add_development_dependency "rspec", "~> 3.0"
|
34
34
|
spec.add_development_dependency "pg"
|
35
|
-
spec.add_development_dependency "db-query-matchers", "~> 0.
|
35
|
+
spec.add_development_dependency "db-query-matchers", "~> 0.12.0"
|
36
36
|
spec.add_development_dependency "pry"
|
37
37
|
spec.add_development_dependency "pry-byebug"
|
38
38
|
spec.add_development_dependency "appraisal", "~> 2.2.0"
|
39
39
|
|
40
|
-
spec.add_dependency "rails", ">= 6.1", "< 7.
|
40
|
+
spec.add_dependency "rails", ">= 6.1", "< 7.2"
|
41
41
|
spec.add_dependency "relation_to_struct", ">= 1.5.1"
|
42
42
|
spec.add_dependency "ruby2_keywords"
|
43
43
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pg_ha_migrations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- celeen
|
@@ -14,7 +14,7 @@ authors:
|
|
14
14
|
autorequire:
|
15
15
|
bindir: exe
|
16
16
|
cert_chain: []
|
17
|
-
date:
|
17
|
+
date: 2024-01-12 00:00:00.000000000 Z
|
18
18
|
dependencies:
|
19
19
|
- !ruby/object:Gem::Dependency
|
20
20
|
name: rake
|
@@ -64,14 +64,14 @@ dependencies:
|
|
64
64
|
requirements:
|
65
65
|
- - "~>"
|
66
66
|
- !ruby/object:Gem::Version
|
67
|
-
version: 0.
|
67
|
+
version: 0.12.0
|
68
68
|
type: :development
|
69
69
|
prerelease: false
|
70
70
|
version_requirements: !ruby/object:Gem::Requirement
|
71
71
|
requirements:
|
72
72
|
- - "~>"
|
73
73
|
- !ruby/object:Gem::Version
|
74
|
-
version: 0.
|
74
|
+
version: 0.12.0
|
75
75
|
- !ruby/object:Gem::Dependency
|
76
76
|
name: pry
|
77
77
|
requirement: !ruby/object:Gem::Requirement
|
@@ -123,7 +123,7 @@ dependencies:
|
|
123
123
|
version: '6.1'
|
124
124
|
- - "<"
|
125
125
|
- !ruby/object:Gem::Version
|
126
|
-
version: '7.
|
126
|
+
version: '7.2'
|
127
127
|
type: :runtime
|
128
128
|
prerelease: false
|
129
129
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -133,7 +133,7 @@ dependencies:
|
|
133
133
|
version: '6.1'
|
134
134
|
- - "<"
|
135
135
|
- !ruby/object:Gem::Version
|
136
|
-
version: '7.
|
136
|
+
version: '7.2'
|
137
137
|
- !ruby/object:Gem::Dependency
|
138
138
|
name: relation_to_struct
|
139
139
|
requirement: !ruby/object:Gem::Requirement
|
@@ -188,15 +188,19 @@ files:
|
|
188
188
|
- gemfiles/.bundle/config
|
189
189
|
- gemfiles/rails_6.1.gemfile
|
190
190
|
- gemfiles/rails_7.0.gemfile
|
191
|
+
- gemfiles/rails_7.1.gemfile
|
191
192
|
- lib/pg_ha_migrations.rb
|
192
193
|
- lib/pg_ha_migrations/allowed_versions.rb
|
193
194
|
- lib/pg_ha_migrations/blocking_database_transactions.rb
|
194
195
|
- lib/pg_ha_migrations/blocking_database_transactions_reporter.rb
|
195
196
|
- lib/pg_ha_migrations/dependent_objects_checks.rb
|
197
|
+
- lib/pg_ha_migrations/hacks/add_index_on_only.rb
|
196
198
|
- lib/pg_ha_migrations/hacks/cleanup_unnecessary_output.rb
|
197
199
|
- lib/pg_ha_migrations/hacks/disable_ddl_transaction.rb
|
200
|
+
- lib/pg_ha_migrations/lock_mode.rb
|
198
201
|
- lib/pg_ha_migrations/partman_config.rb
|
199
202
|
- lib/pg_ha_migrations/railtie.rb
|
203
|
+
- lib/pg_ha_migrations/relation.rb
|
200
204
|
- lib/pg_ha_migrations/safe_statements.rb
|
201
205
|
- lib/pg_ha_migrations/unsafe_statements.rb
|
202
206
|
- lib/pg_ha_migrations/version.rb
|