pg_ha_migrations 1.6.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 +14 -71
- data/.pryrc +6 -6
- data/.ruby-version +1 -1
- data/Appraisals +6 -18
- data/Dockerfile +11 -0
- data/README.md +249 -8
- data/bin/setup +2 -5
- data/docker-compose.yml +11 -0
- data/gemfiles/rails_6.1.gemfile +1 -1
- data/gemfiles/rails_7.0.gemfile +1 -1
- data/gemfiles/{rails_5.1.gemfile → rails_7.1.gemfile} +1 -1
- 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/partman_config.rb +11 -0
- data/lib/pg_ha_migrations/relation.rb +155 -0
- data/lib/pg_ha_migrations/safe_statements.rb +333 -20
- data/lib/pg_ha_migrations/unsafe_statements.rb +14 -0
- data/lib/pg_ha_migrations/version.rb +1 -1
- data/lib/pg_ha_migrations.rb +26 -3
- data/pg_ha_migrations.gemspec +2 -2
- metadata +16 -13
- data/gemfiles/rails_5.0.gemfile +0 -7
- data/gemfiles/rails_5.2.gemfile +0 -7
- data/gemfiles/rails_6.0.gemfile +0 -7
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
@@ -5,88 +5,31 @@ jobs:
|
|
5
5
|
strategy:
|
6
6
|
matrix:
|
7
7
|
pg:
|
8
|
-
- 9.6
|
9
|
-
- 10
|
10
8
|
- 11
|
11
9
|
- 12
|
10
|
+
- 13
|
11
|
+
- 14
|
12
|
+
- 15
|
13
|
+
- 16
|
12
14
|
ruby:
|
13
|
-
-
|
15
|
+
- "3.0"
|
16
|
+
- "3.1"
|
17
|
+
- "3.2"
|
14
18
|
gemfile:
|
15
|
-
- rails_5.0
|
16
|
-
- rails_5.1
|
17
|
-
- rails_5.2
|
18
|
-
- rails_6.0
|
19
19
|
- rails_6.1
|
20
20
|
- rails_7.0
|
21
|
-
|
22
|
-
|
23
|
-
ruby: 3.0
|
24
|
-
pg: 9.6
|
25
|
-
- gemfile: rails_6.1
|
26
|
-
ruby: 3.0
|
27
|
-
pg: 10
|
28
|
-
- gemfile: rails_6.1
|
29
|
-
ruby: 3.0
|
30
|
-
pg: 11
|
31
|
-
- gemfile: rails_6.1
|
32
|
-
ruby: 3.0
|
33
|
-
pg: 12
|
34
|
-
- gemfile: rails_6.1
|
35
|
-
ruby: 3.1
|
36
|
-
pg: 9.6
|
37
|
-
- gemfile: rails_6.1
|
38
|
-
ruby: 3.1
|
39
|
-
pg: 10
|
40
|
-
- gemfile: rails_6.1
|
41
|
-
ruby: 3.1
|
42
|
-
pg: 11
|
43
|
-
- gemfile: rails_6.1
|
44
|
-
ruby: 3.1
|
45
|
-
pg: 12
|
46
|
-
- gemfile: rails_7.0
|
47
|
-
ruby: 3.0
|
48
|
-
pg: 9.6
|
49
|
-
- gemfile: rails_7.0
|
50
|
-
ruby: 3.0
|
51
|
-
pg: 10
|
52
|
-
- gemfile: rails_7.0
|
53
|
-
ruby: 3.0
|
54
|
-
pg: 11
|
55
|
-
- gemfile: rails_7.0
|
56
|
-
ruby: 3.0
|
57
|
-
pg: 12
|
58
|
-
- gemfile: rails_7.0
|
59
|
-
ruby: 3.1
|
60
|
-
pg: 9.6
|
61
|
-
- gemfile: rails_7.0
|
62
|
-
ruby: 3.1
|
63
|
-
pg: 10
|
64
|
-
- gemfile: rails_7.0
|
65
|
-
ruby: 3.1
|
66
|
-
pg: 11
|
67
|
-
- gemfile: rails_7.0
|
68
|
-
ruby: 3.1
|
69
|
-
pg: 12
|
70
|
-
name: PostgreSQL ${{ matrix.pg }}
|
21
|
+
- rails_7.1
|
22
|
+
name: PostgreSQL ${{ matrix.pg }} - Ruby ${{ matrix.ruby }} - ${{ matrix.gemfile }}
|
71
23
|
runs-on: ubuntu-latest
|
72
24
|
env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
|
73
25
|
BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
|
74
26
|
ImageOS: ubuntu20
|
75
|
-
services:
|
76
|
-
postgresql:
|
77
|
-
image: postgres:${{ matrix.pg }}
|
78
|
-
env:
|
79
|
-
POSTGRES_PASSWORD: postgres
|
80
|
-
# Set health checks to wait until postgres has started
|
81
|
-
options: >-
|
82
|
-
--health-cmd pg_isready
|
83
|
-
--health-interval 10s
|
84
|
-
--health-timeout 5s
|
85
|
-
--health-retries 5
|
86
|
-
ports:
|
87
|
-
- 5432:5432
|
88
27
|
steps:
|
89
|
-
- uses: actions/checkout@
|
28
|
+
- uses: actions/checkout@v3
|
29
|
+
- name: Build postgres image and start the container
|
30
|
+
run: docker-compose up -d --build
|
31
|
+
env:
|
32
|
+
PGVERSION: ${{ matrix.pg }}
|
90
33
|
- name: Setup Ruby using .ruby-version file
|
91
34
|
uses: ruby/setup-ruby@v1
|
92
35
|
with:
|
data/.pryrc
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
1
|
+
require "pry-byebug"
|
2
|
+
|
3
|
+
Pry.commands.alias_command 'c', 'continue'
|
4
|
+
Pry.commands.alias_command 's', 'step'
|
5
|
+
Pry.commands.alias_command 'n', 'next'
|
6
|
+
Pry.commands.alias_command 'f', 'finish'
|
7
7
|
|
8
8
|
# https://github.com/pry/pry/issues/1275#issuecomment-131969510
|
9
9
|
# Prevent issue where text input does not display on screen in container after typing Ctrl-C in a pry repl
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby-
|
1
|
+
ruby-3.0
|
data/Appraisals
CHANGED
@@ -1,23 +1,11 @@
|
|
1
|
-
appraise "rails-5.0" do
|
2
|
-
gem "rails", "5.0.7.2"
|
3
|
-
end
|
4
|
-
|
5
|
-
appraise "rails-5.1" do
|
6
|
-
gem "rails", "5.1.7"
|
7
|
-
end
|
8
|
-
|
9
|
-
appraise "rails-5.2" do
|
10
|
-
gem "rails", "5.2.3"
|
11
|
-
end
|
12
|
-
|
13
|
-
appraise "rails-6.0" do
|
14
|
-
gem "rails", "6.0.0"
|
15
|
-
end
|
16
|
-
|
17
1
|
appraise "rails-6.1" do
|
18
|
-
gem "rails", "6.1.
|
2
|
+
gem "rails", "6.1.7.6"
|
19
3
|
end
|
20
4
|
|
21
5
|
appraise "rails-7.0" do
|
22
|
-
gem "rails", "7.0.
|
6
|
+
gem "rails", "7.0.8"
|
7
|
+
end
|
8
|
+
|
9
|
+
appraise "rails-7.1" do
|
10
|
+
gem "rails", "7.1.0"
|
23
11
|
end
|
data/Dockerfile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
ARG PGVERSION
|
2
|
+
|
3
|
+
FROM postgres:$PGVERSION-bullseye
|
4
|
+
|
5
|
+
RUN apt-get update && apt-get install -y curl ca-certificates gnupg lsb-release
|
6
|
+
|
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
|
+
|
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
|
+
|
11
|
+
RUN apt update && apt-get install -y postgresql-$PG_MAJOR-partman=4.7.4-2.pgdg110+1
|
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# PgHaMigrations
|
2
2
|
|
3
|
-
[![Build Status](https://
|
3
|
+
[![Build Status](https://github.com/braintree/pg_ha_migrations/actions/workflows/ci.yml/badge.svg)](https://github.com/braintree/pg_ha_migrations/actions/workflows/ci.yml?query=branch%3Amaster+)
|
4
4
|
|
5
|
-
We've documented our learned best practices for applying schema changes without downtime in the post [PostgreSQL at Scale: Database Schema Changes Without Downtime](https://medium.com/
|
5
|
+
We've documented our learned best practices for applying schema changes without downtime in the post [PostgreSQL at Scale: Database Schema Changes Without Downtime](https://medium.com/paypal-tech/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680) on the [PayPal Technology Blog](https://medium.com/paypal-tech). Many of the approaches we take and choices we've made are explained in much greater depth there than in this README.
|
6
6
|
|
7
7
|
Internally we apply those best practices to our Rails applications through this gem which updates ActiveRecord migrations to clearly delineate safe and unsafe DDL as well as provide safe alternatives where possible.
|
8
8
|
|
@@ -34,7 +34,7 @@ Or install it yourself as:
|
|
34
34
|
|
35
35
|
### Rollback
|
36
36
|
|
37
|
-
Because we require that ["Rollback strategies do not involve reverting the database schema to its previous version"](https://medium.com/
|
37
|
+
Because we require that ["Rollback strategies do not involve reverting the database schema to its previous version"](https://medium.com/paypal-tech/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680#360a), PgHaMigrations does not support ActiveRecord's automatic migration rollback capability.
|
38
38
|
|
39
39
|
Instead we write all of our migrations with only an `def up` method like:
|
40
40
|
|
@@ -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/
|
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.
|
@@ -220,11 +268,174 @@ Drop any (not just `CHECK`) constraint.
|
|
220
268
|
unsafe_remove_constraint :table, name: :constraint_table_on_column_like_example
|
221
269
|
```
|
222
270
|
|
271
|
+
#### safe\_create\_partitioned\_table
|
272
|
+
|
273
|
+
Safely create a new partitioned table using [declaritive partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html#DDL-PARTITIONING-DECLARATIVE).
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
# list partitioned table using single column as partition key
|
277
|
+
safe_create_partitioned_table :table, type: :list, partition_key: :example_column do |t|
|
278
|
+
t.text :example_column, null: false
|
279
|
+
end
|
280
|
+
|
281
|
+
# range partitioned table using multiple columns as partition key
|
282
|
+
safe_create_partitioned_table :table, type: :range, partition_key: [:example_column_a, :example_column_b] do |t|
|
283
|
+
t.integer :example_column_a, null: false
|
284
|
+
t.integer :example_column_b, null: false
|
285
|
+
end
|
286
|
+
|
287
|
+
# hash partitioned table using expression as partition key
|
288
|
+
safe_create_partitioned_table :table, :type: :hash, partition_key: ->{ "(example_column::date)" } do |t|
|
289
|
+
t.datetime :example_column, null: false
|
290
|
+
end
|
291
|
+
```
|
292
|
+
|
293
|
+
The identifier column type is `bigserial` by default. This can be overridden, as you would in `safe_create_table`, by setting the `id` argument:
|
294
|
+
|
295
|
+
```ruby
|
296
|
+
safe_create_partitioned_table :table, id: :serial, type: :range, partition_key: :example_column do |t|
|
297
|
+
t.date :example_column, null: false
|
298
|
+
end
|
299
|
+
```
|
300
|
+
|
301
|
+
In PostgreSQL 11+, primary key constraints are supported on partitioned tables given the partition key is included. On supported versions, the primary key is inferred by default (see [available options](#available-options)). This functionality can be overridden by setting the `infer_primary_key` argument.
|
302
|
+
|
303
|
+
```ruby
|
304
|
+
# primary key will be (id, example_column)
|
305
|
+
safe_create_partitioned_table :table, type: :range, partition_key: :example_column do |t|
|
306
|
+
t.date :example_column, null: false
|
307
|
+
end
|
308
|
+
|
309
|
+
# primary key will not be created
|
310
|
+
safe_create_partitioned_table :table, type: :range, partition_key: :example_column, infer_primary_key: false do |t|
|
311
|
+
t.date :example_column, null: false
|
312
|
+
end
|
313
|
+
```
|
314
|
+
|
315
|
+
#### safe\_partman\_create\_parent
|
316
|
+
|
317
|
+
Safely configure a partitioned table to be managed by [pg\_partman](https://github.com/pgpartman/pg_partman).
|
318
|
+
|
319
|
+
This method calls the [create\_parent](https://github.com/pgpartman/pg_partman/blob/master/doc/pg_partman.md#creation-functions) partman function with some reasonable defaults and a subset of user-defined overrides.
|
320
|
+
|
321
|
+
The first (and only) positional argument maps to `p_parent_table` in the `create_parent` function.
|
322
|
+
|
323
|
+
The rest are keyword args with the following mappings:
|
324
|
+
|
325
|
+
- `partition_key` -> `p_control`. Required: `true`
|
326
|
+
- `interval` -> `p_interval`. Required: `true`
|
327
|
+
- `template_table` -> `p_template_table`. Required: `false`. Partman will create a template table if not defined.
|
328
|
+
- `premake` -> `p_premake`. Required: `false`. Partman defaults to `4`.
|
329
|
+
- `start_partition` -> `p_start_partition`. Required: `false`. Partman defaults to the current timestamp.
|
330
|
+
|
331
|
+
Note that we have chosen to require PostgreSQL 11+ and hardcode `p_type` to `native` for simplicity, as previous PostgreSQL versions are end-of-life.
|
332
|
+
|
333
|
+
Additionally, this method allows you to configure a subset of attributes on the record stored in the [part\_config](https://github.com/pgpartman/pg_partman/blob/master/doc/pg_partman.md#tables) table.
|
334
|
+
These options are delegated to the `unsafe_partman_update_config` method to update the record:
|
335
|
+
|
336
|
+
- `infinite_time_partitions`. Partman defaults this to `false` but we default to `true`
|
337
|
+
- `inherit_privileges`. Partman defaults this to `false` but we default to `true`
|
338
|
+
- `retention`. Partman defaults this to `null`
|
339
|
+
- `retention_keep_table`. Partman defaults this to `true`
|
340
|
+
|
341
|
+
With only the required args:
|
342
|
+
|
343
|
+
```ruby
|
344
|
+
safe_create_partitioned_table :table, type: :range, partition_key: :created_at do |t|
|
345
|
+
t.timestamps null: false
|
346
|
+
end
|
347
|
+
|
348
|
+
safe_partman_create_parent :table, partition_key: :created_at, interval: "weekly"
|
349
|
+
```
|
350
|
+
|
351
|
+
With custom overrides:
|
352
|
+
|
353
|
+
```ruby
|
354
|
+
safe_create_partitioned_table :table, type: :range, partition_key: :created_at do |t|
|
355
|
+
t.timestamps null: false
|
356
|
+
t.text :some_column
|
357
|
+
end
|
358
|
+
|
359
|
+
# Partman will reference the template table to create unique indexes on child tables
|
360
|
+
safe_create_table :table_template, id: false do |t|
|
361
|
+
t.text :some_column, index: {unique: true}
|
362
|
+
end
|
363
|
+
|
364
|
+
safe_partman_create_parent :table,
|
365
|
+
partition_key: :created_at,
|
366
|
+
interval: "weekly",
|
367
|
+
template_table: :table_template,
|
368
|
+
premake: 10,
|
369
|
+
start_partition: Time.current + 1.month,
|
370
|
+
infinite_time_partitions: false,
|
371
|
+
inherit_privileges: false
|
372
|
+
```
|
373
|
+
|
374
|
+
#### unsafe\_partman\_create\_parent
|
375
|
+
|
376
|
+
We have chosen to flag the use of `retention` and `retention_keep_table` as an unsafe operation.
|
377
|
+
While we recognize that these options are useful, we think they fit in the same category as `drop_table` and `rename_table`, and are therefore unsafe from an application perspective.
|
378
|
+
If you wish to define these options, you must use this method.
|
379
|
+
|
380
|
+
```ruby
|
381
|
+
safe_create_partitioned_table :table, type: :range, partition_key: :created_at do |t|
|
382
|
+
t.timestamps null: false
|
383
|
+
end
|
384
|
+
|
385
|
+
unsafe_partman_create_parent :table,
|
386
|
+
partition_key: :created_at,
|
387
|
+
interval: "weekly",
|
388
|
+
retention: "60 days",
|
389
|
+
retention_keep_table: false
|
390
|
+
```
|
391
|
+
|
392
|
+
#### safe\_partman\_update\_config
|
393
|
+
|
394
|
+
There are some partitioning options that cannot be set in the call to `create_parent` and are only available in the `part_config` table.
|
395
|
+
As mentioned previously, you can specify these args in the call to `safe_partman_create_parent` or `unsafe_partman_create_parent` which will be delegated to this method.
|
396
|
+
Calling this method directly will be useful if you need to modify your partitioned table after the fact.
|
397
|
+
|
398
|
+
Allowed keyword args:
|
399
|
+
|
400
|
+
- `infinite_time_partitions`
|
401
|
+
- `inherit_privileges`
|
402
|
+
- `premake`
|
403
|
+
- `retention`
|
404
|
+
- `retention_keep_table`
|
405
|
+
|
406
|
+
Note that we detect if the value of `inherit_privileges` is changing and will automatically call `safe_partman_reapply_privileges` to ensure permissions are propagated to existing child partitions.
|
407
|
+
|
408
|
+
```ruby
|
409
|
+
safe_partman_update_config :table,
|
410
|
+
infinite_time_partitions: false,
|
411
|
+
inherit_privileges: false,
|
412
|
+
premake: 10
|
413
|
+
```
|
414
|
+
|
415
|
+
#### unsafe\_partman\_update\_config
|
416
|
+
|
417
|
+
As with creating a partman parent table, we have chosen to flag the use of `retention` and `retention_keep_table` as an unsafe operation.
|
418
|
+
If you wish to define these options, you must use this method.
|
419
|
+
|
420
|
+
```ruby
|
421
|
+
unsafe_partman_update_config :table,
|
422
|
+
retention: "60 days",
|
423
|
+
retention_keep_table: false
|
424
|
+
```
|
425
|
+
|
426
|
+
#### safe\_partman\_reapply\_privileges
|
427
|
+
|
428
|
+
If your partitioned table is configured with `inherit_privileges` set to `true`, use this method after granting new roles / privileges on the parent table to ensure permissions are propagated to existing child partitions.
|
429
|
+
|
430
|
+
```ruby
|
431
|
+
safe_partman_reapply_privileges :table
|
432
|
+
```
|
433
|
+
|
223
434
|
### Utilities
|
224
435
|
|
225
436
|
#### safely\_acquire\_lock\_for\_table
|
226
437
|
|
227
|
-
Safely acquire
|
438
|
+
Safely acquire an access exclusive lock for a table.
|
228
439
|
|
229
440
|
```ruby
|
230
441
|
safely_acquire_lock_for_table(:table) do
|
@@ -232,6 +443,19 @@ safely_acquire_lock_for_table(:table) do
|
|
232
443
|
end
|
233
444
|
```
|
234
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
|
+
|
235
459
|
#### adjust\_lock\_timeout
|
236
460
|
|
237
461
|
Adjust lock timeout.
|
@@ -260,6 +484,22 @@ Set maintenance work mem.
|
|
260
484
|
safe_set_maintenance_work_mem_gb 1
|
261
485
|
```
|
262
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
|
+
|
263
503
|
### Configuration
|
264
504
|
|
265
505
|
The gem can be configured in an initializer.
|
@@ -274,8 +514,9 @@ end
|
|
274
514
|
|
275
515
|
- `disable_default_migration_methods`: If true, the default implementations of DDL changes in `ActiveRecord::Migration` and the PostgreSQL adapter will be overridden by implementations that raise a `PgHaMigrations::UnsafeMigrationError`. Default: `true`
|
276
516
|
- `check_for_dependent_objects`: If true, some `unsafe_*` migration methods will raise a `PgHaMigrations::UnsafeMigrationError` if any dependent objects exist. Default: `false`
|
277
|
-
- `prefer_single_step_column_addition_with_default`: If
|
278
|
-
-
|
517
|
+
- `prefer_single_step_column_addition_with_default`: If true, raise an error when adding a column and separately setting a constant default value for that column in the same migration. Default: `false`
|
518
|
+
- `allow_force_create_table`: If false, the `force: true` option to ActiveRecord's `create_table` method is disallowed. Default: `true`
|
519
|
+
- `infer_primary_key_on_partitioned_tables`: If true, the primary key for partitioned tables will be inferred on PostgreSQL 11+ databases (identifier column + partition key columns). Default: `true`
|
279
520
|
|
280
521
|
### Rake Tasks
|
281
522
|
|
@@ -305,7 +546,7 @@ Running tests will automatically create a test database in the locally running P
|
|
305
546
|
|
306
547
|
To install this gem onto your local machine, run `bundle exec rake install`.
|
307
548
|
|
308
|
-
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).
|
549
|
+
To release a new version, update the version number in `version.rb`, commit the change, 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).
|
309
550
|
|
310
551
|
Note: if while releasing the gem you get the error ``Your rubygems.org credentials aren't set. Run `gem push` to set them.`` you can more simply run `gem signin`.
|
311
552
|
|
data/bin/setup
CHANGED
@@ -2,14 +2,11 @@
|
|
2
2
|
set -euo pipefail
|
3
3
|
IFS=$'\n\t'
|
4
4
|
set -vx
|
5
|
-
export PGPASSWORD="${PGPASSWORD:-postgres}"
|
6
|
-
PGVERSION="${PGVERSION:-13}"
|
7
|
-
|
8
5
|
|
9
6
|
bundle install
|
10
7
|
bundle exec appraisal install
|
11
8
|
|
12
9
|
# Do any other automated setup that you need to do here
|
13
10
|
|
14
|
-
# Launch a blank postgres image for testing
|
15
|
-
docker
|
11
|
+
# Launch a blank postgres image with partman for testing
|
12
|
+
docker-compose up -d --build
|
data/docker-compose.yml
ADDED
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)
|