pg_ha_migrations 1.6.0 → 1.7.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 +12 -71
- data/.pryrc +6 -6
- data/.ruby-version +1 -1
- data/Appraisals +1 -17
- data/Dockerfile +11 -0
- data/README.md +171 -7
- data/bin/setup +2 -5
- data/docker-compose.yml +11 -0
- data/lib/pg_ha_migrations/partman_config.rb +11 -0
- data/lib/pg_ha_migrations/safe_statements.rb +226 -14
- data/lib/pg_ha_migrations/unsafe_statements.rb +4 -0
- data/lib/pg_ha_migrations/version.rb +1 -1
- data/lib/pg_ha_migrations.rb +6 -3
- data/pg_ha_migrations.gemspec +1 -1
- metadata +8 -9
- data/gemfiles/rails_5.0.gemfile +0 -7
- data/gemfiles/rails_5.1.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: 9111677a3084d7b769a43f1c7078822c9ae84420443893f54dc1e228bfa037d1
|
4
|
+
data.tar.gz: 7f1db28ace4c6416980c1f5c3461669f43bd7cb116996c2af7759d48515a8899
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 30c567438be90db49faf206696a51176ea397d448913194ac7b2de02349294cced3d3c94fc08e3b5225eb7b1678d997af716de4f3da9cf15491745ae92af2a22
|
7
|
+
data.tar.gz: dba6fa0e40b690a838a2ae26bd48a0cf18b0acda0922a74a14ae8dec295f1c3a48181a11f0ed4bc6cb46f24e9dadd03dcad86ec805c83c00ecd0cf998176cead
|
data/.github/workflows/ci.yml
CHANGED
@@ -5,88 +5,29 @@ 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
|
12
13
|
ruby:
|
13
|
-
-
|
14
|
+
- 3.0
|
15
|
+
- 3.1
|
16
|
+
- 3.2
|
14
17
|
gemfile:
|
15
|
-
- rails_5.0
|
16
|
-
- rails_5.1
|
17
|
-
- rails_5.2
|
18
|
-
- rails_6.0
|
19
18
|
- rails_6.1
|
20
19
|
- rails_7.0
|
21
|
-
|
22
|
-
- gemfile: rails_6.1
|
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 }}
|
20
|
+
name: PostgreSQL ${{ matrix.pg }} - Ruby ${{ matrix.ruby }} - ${{ matrix.gemfile }}
|
71
21
|
runs-on: ubuntu-latest
|
72
22
|
env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
|
73
23
|
BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
|
74
24
|
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
25
|
steps:
|
89
|
-
- uses: actions/checkout@
|
26
|
+
- uses: actions/checkout@v3
|
27
|
+
- name: Build postgres image and start the container
|
28
|
+
run: docker-compose up -d --build
|
29
|
+
env:
|
30
|
+
PGVERSION: ${{ matrix.pg }}
|
90
31
|
- name: Setup Ruby using .ruby-version file
|
91
32
|
uses: ruby/setup-ruby@v1
|
92
33
|
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,7 @@
|
|
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
2
|
gem "rails", "6.1.0"
|
19
3
|
end
|
20
4
|
|
21
5
|
appraise "rails-7.0" do
|
22
|
-
gem "rails", "7.0.
|
6
|
+
gem "rails", "7.0.1"
|
23
7
|
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 http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list
|
10
|
+
|
11
|
+
RUN apt update && apt-get install -y postgresql-$PG_MAJOR-partman
|
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.rb:8) 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
|
|
@@ -220,6 +220,169 @@ Drop any (not just `CHECK`) constraint.
|
|
220
220
|
unsafe_remove_constraint :table, name: :constraint_table_on_column_like_example
|
221
221
|
```
|
222
222
|
|
223
|
+
#### safe\_create\_partitioned\_table
|
224
|
+
|
225
|
+
Safely create a new partitioned table using [declaritive partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html#DDL-PARTITIONING-DECLARATIVE).
|
226
|
+
|
227
|
+
```ruby
|
228
|
+
# list partitioned table using single column as partition key
|
229
|
+
safe_create_partitioned_table :table, type: :list, partition_key: :example_column do |t|
|
230
|
+
t.text :example_column, null: false
|
231
|
+
end
|
232
|
+
|
233
|
+
# range partitioned table using multiple columns as partition key
|
234
|
+
safe_create_partitioned_table :table, type: :range, partition_key: [:example_column_a, :example_column_b] do |t|
|
235
|
+
t.integer :example_column_a, null: false
|
236
|
+
t.integer :example_column_b, null: false
|
237
|
+
end
|
238
|
+
|
239
|
+
# hash partitioned table using expression as partition key
|
240
|
+
safe_create_partitioned_table :table, :type: :hash, partition_key: ->{ "(example_column::date)" } do |t|
|
241
|
+
t.datetime :example_column, null: false
|
242
|
+
end
|
243
|
+
```
|
244
|
+
|
245
|
+
The identifier column type is `bigserial` by default. This can be overridden, as you would in `safe_create_table`, by setting the `id` argument:
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
safe_create_partitioned_table :table, id: :serial, type: :range, partition_key: :example_column do |t|
|
249
|
+
t.date :example_column, null: false
|
250
|
+
end
|
251
|
+
```
|
252
|
+
|
253
|
+
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.
|
254
|
+
|
255
|
+
```ruby
|
256
|
+
# primary key will be (id, example_column)
|
257
|
+
safe_create_partitioned_table :table, type: :range, partition_key: :example_column do |t|
|
258
|
+
t.date :example_column, null: false
|
259
|
+
end
|
260
|
+
|
261
|
+
# primary key will not be created
|
262
|
+
safe_create_partitioned_table :table, type: :range, partition_key: :example_column, infer_primary_key: false do |t|
|
263
|
+
t.date :example_column, null: false
|
264
|
+
end
|
265
|
+
```
|
266
|
+
|
267
|
+
#### safe\_partman\_create\_parent
|
268
|
+
|
269
|
+
Safely configure a partitioned table to be managed by [pg\_partman](https://github.com/pgpartman/pg_partman).
|
270
|
+
|
271
|
+
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.
|
272
|
+
|
273
|
+
The first (and only) positional argument maps to `p_parent_table` in the `create_parent` function.
|
274
|
+
|
275
|
+
The rest are keyword args with the following mappings:
|
276
|
+
|
277
|
+
- `partition_key` -> `p_control`. Required: `true`
|
278
|
+
- `interval` -> `p_interval`. Required: `true`
|
279
|
+
- `template_table` -> `p_template_table`. Required: `false`. Partman will create a template table if not defined.
|
280
|
+
- `premake` -> `p_premake`. Required: `false`. Partman defaults to `4`.
|
281
|
+
- `start_partition` -> `p_start_partition`. Required: `false`. Partman defaults to the current timestamp.
|
282
|
+
|
283
|
+
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.
|
284
|
+
|
285
|
+
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.
|
286
|
+
These options are delegated to the `unsafe_partman_update_config` method to update the record:
|
287
|
+
|
288
|
+
- `infinite_time_partitions`. Partman defaults this to `false` but we default to `true`
|
289
|
+
- `inherit_privileges`. Partman defaults this to `false` but we default to `true`
|
290
|
+
- `retention`. Partman defaults this to `null`
|
291
|
+
- `retention_keep_table`. Partman defaults this to `true`
|
292
|
+
|
293
|
+
With only the required args:
|
294
|
+
|
295
|
+
```ruby
|
296
|
+
safe_create_partitioned_table :table, type: :range, partition_key: :created_at do |t|
|
297
|
+
t.timestamps null: false
|
298
|
+
end
|
299
|
+
|
300
|
+
safe_partman_create_parent :table, partition_key: :created_at, interval: "weekly"
|
301
|
+
```
|
302
|
+
|
303
|
+
With custom overrides:
|
304
|
+
|
305
|
+
```ruby
|
306
|
+
safe_create_partitioned_table :table, type: :range, partition_key: :created_at do |t|
|
307
|
+
t.timestamps null: false
|
308
|
+
t.text :some_column
|
309
|
+
end
|
310
|
+
|
311
|
+
# Partman will reference the template table to create unique indexes on child tables
|
312
|
+
safe_create_table :table_template, id: false do |t|
|
313
|
+
t.text :some_column, index: {unique: true}
|
314
|
+
end
|
315
|
+
|
316
|
+
safe_partman_create_parent :table,
|
317
|
+
partition_key: :created_at,
|
318
|
+
interval: "weekly",
|
319
|
+
template_table: :table_template,
|
320
|
+
premake: 10,
|
321
|
+
start_partition: Time.current + 1.month,
|
322
|
+
infinite_time_partitions: false,
|
323
|
+
inherit_privileges: false
|
324
|
+
```
|
325
|
+
|
326
|
+
#### unsafe\_partman\_create\_parent
|
327
|
+
|
328
|
+
We have chosen to flag the use of `retention` and `retention_keep_table` as an unsafe operation.
|
329
|
+
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.
|
330
|
+
If you wish to define these options, you must use this method.
|
331
|
+
|
332
|
+
```ruby
|
333
|
+
safe_create_partitioned_table :table, type: :range, partition_key: :created_at do |t|
|
334
|
+
t.timestamps null: false
|
335
|
+
end
|
336
|
+
|
337
|
+
unsafe_partman_create_parent :table,
|
338
|
+
partition_key: :created_at,
|
339
|
+
interval: "weekly",
|
340
|
+
retention: "60 days",
|
341
|
+
retention_keep_table: false
|
342
|
+
```
|
343
|
+
|
344
|
+
#### safe\_partman\_update\_config
|
345
|
+
|
346
|
+
There are some partitioning options that cannot be set in the call to `create_parent` and are only available in the `part_config` table.
|
347
|
+
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.
|
348
|
+
Calling this method directly will be useful if you need to modify your partitioned table after the fact.
|
349
|
+
|
350
|
+
Allowed keyword args:
|
351
|
+
|
352
|
+
- `infinite_time_partitions`
|
353
|
+
- `inherit_privileges`
|
354
|
+
- `premake`
|
355
|
+
- `retention`
|
356
|
+
- `retention_keep_table`
|
357
|
+
|
358
|
+
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.
|
359
|
+
|
360
|
+
```ruby
|
361
|
+
safe_partman_update_config :table,
|
362
|
+
infinite_time_partitions: false,
|
363
|
+
inherit_privileges: false,
|
364
|
+
premake: 10
|
365
|
+
```
|
366
|
+
|
367
|
+
#### unsafe\_partman\_update\_config
|
368
|
+
|
369
|
+
As with creating a partman parent table, we have chosen to flag the use of `retention` and `retention_keep_table` as an unsafe operation.
|
370
|
+
If you wish to define these options, you must use this method.
|
371
|
+
|
372
|
+
```ruby
|
373
|
+
unsafe_partman_update_config :table,
|
374
|
+
retention: "60 days",
|
375
|
+
retention_keep_table: false
|
376
|
+
```
|
377
|
+
|
378
|
+
#### safe\_partman\_reapply\_privileges
|
379
|
+
|
380
|
+
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.
|
381
|
+
|
382
|
+
```ruby
|
383
|
+
safe_partman_reapply_privileges :table
|
384
|
+
```
|
385
|
+
|
223
386
|
### Utilities
|
224
387
|
|
225
388
|
#### safely\_acquire\_lock\_for\_table
|
@@ -274,8 +437,9 @@ end
|
|
274
437
|
|
275
438
|
- `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
439
|
- `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
|
-
-
|
440
|
+
- `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`
|
441
|
+
- `allow_force_create_table`: If false, the `force: true` option to ActiveRecord's `create_table` method is disallowed. Default: `true`
|
442
|
+
- `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
443
|
|
280
444
|
### Rake Tasks
|
281
445
|
|
@@ -305,7 +469,7 @@ Running tests will automatically create a test database in the locally running P
|
|
305
469
|
|
306
470
|
To install this gem onto your local machine, run `bundle exec rake install`.
|
307
471
|
|
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).
|
472
|
+
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
473
|
|
310
474
|
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
475
|
|
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
@@ -0,0 +1,11 @@
|
|
1
|
+
# This is an internal class that is not meant to be used directly
|
2
|
+
class PgHaMigrations::PartmanConfig < ActiveRecord::Base
|
3
|
+
self.primary_key = :parent_table
|
4
|
+
|
5
|
+
# This method is called by unsafe_partman_update_config to set the fully
|
6
|
+
# qualified table name, as partman is often installed in a schema that
|
7
|
+
# is not included the application's search path
|
8
|
+
def self.schema=(schema)
|
9
|
+
self.table_name = "#{schema}.part_config"
|
10
|
+
end
|
11
|
+
end
|
@@ -1,4 +1,14 @@
|
|
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
|
+
|
2
12
|
def safe_added_columns_without_default_value
|
3
13
|
@safe_added_columns_without_default_value ||= []
|
4
14
|
end
|
@@ -168,20 +178,6 @@ module PgHaMigrations::SafeStatements
|
|
168
178
|
unsafe_add_check_constraint(table, expression, name: name, validate: false)
|
169
179
|
end
|
170
180
|
|
171
|
-
def unsafe_add_check_constraint(table, expression, name:, validate: true)
|
172
|
-
raise ArgumentError, "Expected <name> to be present" unless name.present?
|
173
|
-
|
174
|
-
quoted_table_name = connection.quote_table_name(table)
|
175
|
-
quoted_constraint_name = connection.quote_table_name(name)
|
176
|
-
sql = "ALTER TABLE #{quoted_table_name} ADD CONSTRAINT #{quoted_constraint_name} CHECK (#{expression}) #{validate ? "" : "NOT VALID"}"
|
177
|
-
|
178
|
-
safely_acquire_lock_for_table(table) do
|
179
|
-
say_with_time "add_check_constraint(#{table.inspect}, #{expression.inspect}, name: #{name.inspect}, validate: #{validate.inspect})" do
|
180
|
-
connection.execute(sql)
|
181
|
-
end
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
181
|
def safe_validate_check_constraint(table, name:)
|
186
182
|
raise ArgumentError, "Expected <name> to be present" unless name.present?
|
187
183
|
|
@@ -224,6 +220,222 @@ module PgHaMigrations::SafeStatements
|
|
224
220
|
end
|
225
221
|
end
|
226
222
|
|
223
|
+
def safe_create_partitioned_table(table, partition_key:, type:, infer_primary_key: nil, **options, &block)
|
224
|
+
raise ArgumentError, "Expected <partition_key> to be present" unless partition_key.present?
|
225
|
+
|
226
|
+
unless PARTITION_TYPES.include?(type)
|
227
|
+
raise ArgumentError, "Expected <type> to be symbol in #{PARTITION_TYPES} but received #{type.inspect}"
|
228
|
+
end
|
229
|
+
|
230
|
+
if ActiveRecord::Base.connection.postgresql_version < 10_00_00
|
231
|
+
raise PgHaMigrations::InvalidMigrationError, "Native partitioning not supported on Postgres databases before version 10"
|
232
|
+
end
|
233
|
+
|
234
|
+
if type == :hash && ActiveRecord::Base.connection.postgresql_version < 11_00_00
|
235
|
+
raise PgHaMigrations::InvalidMigrationError, "Hash partitioning not supported on Postgres databases before version 11"
|
236
|
+
end
|
237
|
+
|
238
|
+
if infer_primary_key.nil?
|
239
|
+
infer_primary_key = PgHaMigrations.config.infer_primary_key_on_partitioned_tables
|
240
|
+
end
|
241
|
+
|
242
|
+
# Newer versions of Rails will set the primary key column to the type :primary_key.
|
243
|
+
# This performs some extra logic that we can't easily undo which causes problems when
|
244
|
+
# trying to inject the partition key into the PK. Now, it would be nice to lookup the
|
245
|
+
# default primary key type instead of simply using :bigserial, but it doesn't appear
|
246
|
+
# that we have access to the Rails configuration from within our migrations.
|
247
|
+
if options[:id].nil? || options[:id] == :primary_key
|
248
|
+
options[:id] = :bigserial
|
249
|
+
end
|
250
|
+
|
251
|
+
quoted_partition_key = if partition_key.is_a?(Proc)
|
252
|
+
# Lambda syntax, like in other migration methods, implies an expression that
|
253
|
+
# cannot be easily sanitized.
|
254
|
+
#
|
255
|
+
# e.g ->{ "(created_at::date)" }
|
256
|
+
partition_key.call.to_s
|
257
|
+
else
|
258
|
+
# Otherwise, assume key is a column name or array of column names
|
259
|
+
Array.wrap(partition_key).map { |col| connection.quote_column_name(col) }.join(",")
|
260
|
+
end
|
261
|
+
|
262
|
+
options[:options] = "PARTITION BY #{type.upcase} (#{quoted_partition_key})"
|
263
|
+
|
264
|
+
safe_create_table(table, options) do |td|
|
265
|
+
block.call(td) if block
|
266
|
+
|
267
|
+
next unless options[:id]
|
268
|
+
|
269
|
+
pk_columns = td.columns.each_with_object([]) do |col, arr|
|
270
|
+
next unless col.options[:primary_key]
|
271
|
+
|
272
|
+
col.options[:primary_key] = false
|
273
|
+
|
274
|
+
arr << col.name
|
275
|
+
end
|
276
|
+
|
277
|
+
if infer_primary_key && !partition_key.is_a?(Proc) && ActiveRecord::Base.connection.postgresql_version >= 11_00_00
|
278
|
+
td.primary_keys(pk_columns.concat(Array.wrap(partition_key)).map(&:to_s).uniq)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def safe_partman_create_parent(table, **options)
|
284
|
+
if options[:retention].present? || options[:retention_keep_table] == false
|
285
|
+
raise PgHaMigrations::UnsafeMigrationError.new(":retention and/or :retention_keep_table => false can potentially result in data loss if misconfigured. Please use unsafe_partman_create_parent if you want to set these options")
|
286
|
+
end
|
287
|
+
|
288
|
+
unsafe_partman_create_parent(table, **options)
|
289
|
+
end
|
290
|
+
|
291
|
+
def unsafe_partman_create_parent(
|
292
|
+
table,
|
293
|
+
partition_key:,
|
294
|
+
interval:,
|
295
|
+
infinite_time_partitions: true,
|
296
|
+
inherit_privileges: true,
|
297
|
+
premake: nil,
|
298
|
+
start_partition: nil,
|
299
|
+
template_table: nil,
|
300
|
+
retention: nil,
|
301
|
+
retention_keep_table: nil
|
302
|
+
)
|
303
|
+
raise ArgumentError, "Expected <partition_key> to be present" unless partition_key.present?
|
304
|
+
raise ArgumentError, "Expected <interval> to be present" unless interval.present?
|
305
|
+
|
306
|
+
if ActiveRecord::Base.connection.postgresql_version < 11_00_00
|
307
|
+
raise PgHaMigrations::InvalidMigrationError, "Native partitioning with partman not supported on Postgres databases before version 11"
|
308
|
+
end
|
309
|
+
|
310
|
+
formatted_start_partition = nil
|
311
|
+
|
312
|
+
if start_partition.present?
|
313
|
+
if !start_partition.is_a?(Date) && !start_partition.is_a?(Time) && !start_partition.is_a?(DateTime)
|
314
|
+
raise PgHaMigrations::InvalidMigrationError, "Expected <start_partition> to be Date, Time, or DateTime object but received #{start_partition.class}"
|
315
|
+
end
|
316
|
+
|
317
|
+
formatted_start_partition = if start_partition.respond_to?(:to_fs)
|
318
|
+
start_partition.to_fs(:db)
|
319
|
+
else
|
320
|
+
start_partition.to_s(:db)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
create_parent_options = {
|
325
|
+
parent_table: _fully_qualified_table_name_for_partman(table),
|
326
|
+
template_table: template_table ? _fully_qualified_table_name_for_partman(template_table) : nil,
|
327
|
+
control: partition_key,
|
328
|
+
type: "native",
|
329
|
+
interval: interval,
|
330
|
+
premake: premake,
|
331
|
+
start_partition: formatted_start_partition,
|
332
|
+
}.compact
|
333
|
+
|
334
|
+
create_parent_sql = create_parent_options.map { |k, v| "p_#{k} := #{connection.quote(v)}" }.join(", ")
|
335
|
+
|
336
|
+
log_message = "partman_create_parent(#{table.inspect}, " \
|
337
|
+
"partition_key: #{partition_key.inspect}, " \
|
338
|
+
"interval: #{interval.inspect}, " \
|
339
|
+
"premake: #{premake.inspect}, " \
|
340
|
+
"start_partition: #{start_partition.inspect}, " \
|
341
|
+
"template_table: #{template_table.inspect})"
|
342
|
+
|
343
|
+
say_with_time(log_message) do
|
344
|
+
connection.execute("SELECT #{_quoted_partman_schema}.create_parent(#{create_parent_sql})")
|
345
|
+
end
|
346
|
+
|
347
|
+
update_config_options = {
|
348
|
+
infinite_time_partitions: infinite_time_partitions,
|
349
|
+
inherit_privileges: inherit_privileges,
|
350
|
+
retention: retention,
|
351
|
+
retention_keep_table: retention_keep_table,
|
352
|
+
}.compact
|
353
|
+
|
354
|
+
unsafe_partman_update_config(create_parent_options[:parent_table], **update_config_options)
|
355
|
+
end
|
356
|
+
|
357
|
+
def safe_partman_update_config(table, **options)
|
358
|
+
if options[:retention].present? || options[:retention_keep_table] == false
|
359
|
+
raise PgHaMigrations::UnsafeMigrationError.new(":retention and/or :retention_keep_table => false can potentially result in data loss if misconfigured. Please use unsafe_partman_update_config if you want to set these options")
|
360
|
+
end
|
361
|
+
|
362
|
+
unsafe_partman_update_config(table, **options)
|
363
|
+
end
|
364
|
+
|
365
|
+
def unsafe_partman_update_config(table, **options)
|
366
|
+
invalid_options = options.keys - PARTMAN_UPDATE_CONFIG_OPTIONS
|
367
|
+
|
368
|
+
raise ArgumentError, "Unrecognized argument(s): #{invalid_options}" unless invalid_options.empty?
|
369
|
+
|
370
|
+
PgHaMigrations::PartmanConfig.schema = _quoted_partman_schema
|
371
|
+
|
372
|
+
config = PgHaMigrations::PartmanConfig.find(_fully_qualified_table_name_for_partman(table))
|
373
|
+
|
374
|
+
config.assign_attributes(**options)
|
375
|
+
|
376
|
+
inherit_privileges_changed = config.inherit_privileges_changed?
|
377
|
+
|
378
|
+
say_with_time "partman_update_config(#{table.inspect}, #{options.map { |k,v| "#{k}: #{v.inspect}" }.join(", ")})" do
|
379
|
+
config.save!
|
380
|
+
end
|
381
|
+
|
382
|
+
safe_partman_reapply_privileges(table) if inherit_privileges_changed
|
383
|
+
end
|
384
|
+
|
385
|
+
def safe_partman_reapply_privileges(table)
|
386
|
+
say_with_time "partman_reapply_privileges(#{table.inspect})" do
|
387
|
+
connection.execute("SELECT #{_quoted_partman_schema}.reapply_privileges('#{_fully_qualified_table_name_for_partman(table)}')")
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
def _quoted_partman_schema
|
392
|
+
schema = connection.select_value(<<~SQL)
|
393
|
+
SELECT nspname
|
394
|
+
FROM pg_namespace JOIN pg_extension
|
395
|
+
ON pg_namespace.oid = pg_extension.extnamespace
|
396
|
+
WHERE pg_extension.extname = 'pg_partman'
|
397
|
+
SQL
|
398
|
+
|
399
|
+
raise PgHaMigrations::InvalidMigrationError, "The pg_partman extension is not installed" unless schema.present?
|
400
|
+
|
401
|
+
connection.quote_schema_name(schema)
|
402
|
+
end
|
403
|
+
|
404
|
+
def _fully_qualified_table_name_for_partman(table)
|
405
|
+
identifiers = table.to_s.split(".")
|
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
|
416
|
+
|
417
|
+
schema = connection.select_value(<<~SQL)
|
418
|
+
SELECT schemaname
|
419
|
+
FROM pg_tables
|
420
|
+
WHERE tablename = '#{identifiers.last}' AND schemaname = #{schema_conditional}
|
421
|
+
ORDER BY array_position(current_schemas(false), schemaname)
|
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
|
437
|
+
end
|
438
|
+
|
227
439
|
def _per_migration_caller
|
228
440
|
@_per_migration_caller ||= Kernel.caller
|
229
441
|
end
|
@@ -45,6 +45,8 @@ module PgHaMigrations::UnsafeStatements
|
|
45
45
|
delegate_unsafe_method_to_migration_base_class :remove_index
|
46
46
|
delegate_unsafe_method_to_migration_base_class :add_foreign_key
|
47
47
|
delegate_unsafe_method_to_migration_base_class :remove_foreign_key
|
48
|
+
delegate_unsafe_method_to_migration_base_class :add_check_constraint
|
49
|
+
delegate_unsafe_method_to_migration_base_class :remove_check_constraint
|
48
50
|
|
49
51
|
disable_or_delegate_default_method :create_table, ":create_table is NOT SAFE! Use safe_create_table instead"
|
50
52
|
disable_or_delegate_default_method :add_column, ":add_column is NOT SAFE! Use safe_add_column instead"
|
@@ -61,6 +63,8 @@ module PgHaMigrations::UnsafeStatements
|
|
61
63
|
disable_or_delegate_default_method :remove_index, ":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"
|
62
64
|
disable_or_delegate_default_method :add_foreign_key, ":add_foreign_key is NOT SAFE! Explicitly call :unsafe_add_foreign_key"
|
63
65
|
disable_or_delegate_default_method :remove_foreign_key, ":remove_foreign_key is NOT SAFE! Explicitly call :unsafe_remove_foreign_key"
|
66
|
+
disable_or_delegate_default_method :add_check_constraint, ":add_check_constraint is NOT SAFE! Use :safe_add_unvalidated_check_constraint and then :safe_validate_check_constraint instead"
|
67
|
+
disable_or_delegate_default_method :remove_check_constraint, ":remove_check_constraint is NOT SAFE! Explicitly call :unsafe_remove_check_constraint to proceed"
|
64
68
|
|
65
69
|
def unsafe_create_table(table, options={}, &block)
|
66
70
|
if options[:force] && !PgHaMigrations.config.allow_force_create_table
|
data/lib/pg_ha_migrations.rb
CHANGED
@@ -10,7 +10,8 @@ module PgHaMigrations
|
|
10
10
|
:disable_default_migration_methods,
|
11
11
|
:check_for_dependent_objects,
|
12
12
|
:allow_force_create_table,
|
13
|
-
:prefer_single_step_column_addition_with_default
|
13
|
+
:prefer_single_step_column_addition_with_default,
|
14
|
+
:infer_primary_key_on_partitioned_tables,
|
14
15
|
)
|
15
16
|
|
16
17
|
def self.config
|
@@ -18,7 +19,8 @@ module PgHaMigrations
|
|
18
19
|
true,
|
19
20
|
false,
|
20
21
|
true,
|
21
|
-
false
|
22
|
+
false,
|
23
|
+
true
|
22
24
|
)
|
23
25
|
end
|
24
26
|
|
@@ -42,7 +44,7 @@ module PgHaMigrations
|
|
42
44
|
# raise this error. For example, adding a column without a default and
|
43
45
|
# then setting its default in a second action in a single migration
|
44
46
|
# isn't our documented best practice and will raise this error.
|
45
|
-
BestPracticeError = Class.new(
|
47
|
+
BestPracticeError = Class.new(StandardError)
|
46
48
|
|
47
49
|
# Unsupported migrations use ActiveRecord::Migration features that
|
48
50
|
# we don't support, and therefore will likely have unexpected behavior.
|
@@ -54,6 +56,7 @@ end
|
|
54
56
|
|
55
57
|
require "pg_ha_migrations/blocking_database_transactions"
|
56
58
|
require "pg_ha_migrations/blocking_database_transactions_reporter"
|
59
|
+
require "pg_ha_migrations/partman_config"
|
57
60
|
require "pg_ha_migrations/unsafe_statements"
|
58
61
|
require "pg_ha_migrations/safe_statements"
|
59
62
|
require "pg_ha_migrations/dependent_objects_checks"
|
data/pg_ha_migrations.gemspec
CHANGED
@@ -37,7 +37,7 @@ Gem::Specification.new do |spec|
|
|
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", ">=
|
40
|
+
spec.add_dependency "rails", ">= 6.1", "< 7.1"
|
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.7.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: 2023-08-10 00:00:00.000000000 Z
|
18
18
|
dependencies:
|
19
19
|
- !ruby/object:Gem::Dependency
|
20
20
|
name: rake
|
@@ -120,7 +120,7 @@ dependencies:
|
|
120
120
|
requirements:
|
121
121
|
- - ">="
|
122
122
|
- !ruby/object:Gem::Version
|
123
|
-
version: '
|
123
|
+
version: '6.1'
|
124
124
|
- - "<"
|
125
125
|
- !ruby/object:Gem::Version
|
126
126
|
version: '7.1'
|
@@ -130,7 +130,7 @@ dependencies:
|
|
130
130
|
requirements:
|
131
131
|
- - ">="
|
132
132
|
- !ruby/object:Gem::Version
|
133
|
-
version: '
|
133
|
+
version: '6.1'
|
134
134
|
- - "<"
|
135
135
|
- !ruby/object:Gem::Version
|
136
136
|
version: '7.1'
|
@@ -177,17 +177,15 @@ files:
|
|
177
177
|
- ".ruby-version"
|
178
178
|
- Appraisals
|
179
179
|
- CODE_OF_CONDUCT.md
|
180
|
+
- Dockerfile
|
180
181
|
- Gemfile
|
181
182
|
- LICENSE.txt
|
182
183
|
- README.md
|
183
184
|
- Rakefile
|
184
185
|
- bin/console
|
185
186
|
- bin/setup
|
187
|
+
- docker-compose.yml
|
186
188
|
- gemfiles/.bundle/config
|
187
|
-
- gemfiles/rails_5.0.gemfile
|
188
|
-
- gemfiles/rails_5.1.gemfile
|
189
|
-
- gemfiles/rails_5.2.gemfile
|
190
|
-
- gemfiles/rails_6.0.gemfile
|
191
189
|
- gemfiles/rails_6.1.gemfile
|
192
190
|
- gemfiles/rails_7.0.gemfile
|
193
191
|
- lib/pg_ha_migrations.rb
|
@@ -197,6 +195,7 @@ files:
|
|
197
195
|
- lib/pg_ha_migrations/dependent_objects_checks.rb
|
198
196
|
- lib/pg_ha_migrations/hacks/cleanup_unnecessary_output.rb
|
199
197
|
- lib/pg_ha_migrations/hacks/disable_ddl_transaction.rb
|
198
|
+
- lib/pg_ha_migrations/partman_config.rb
|
200
199
|
- lib/pg_ha_migrations/railtie.rb
|
201
200
|
- lib/pg_ha_migrations/safe_statements.rb
|
202
201
|
- lib/pg_ha_migrations/unsafe_statements.rb
|
@@ -222,7 +221,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
222
221
|
- !ruby/object:Gem::Version
|
223
222
|
version: '0'
|
224
223
|
requirements: []
|
225
|
-
rubygems_version: 3.
|
224
|
+
rubygems_version: 3.2.3
|
226
225
|
signing_key:
|
227
226
|
specification_version: 4
|
228
227
|
summary: Enforces DDL/migration safety in Ruby on Rails project with an emphasis on
|
data/gemfiles/rails_5.0.gemfile
DELETED
data/gemfiles/rails_5.1.gemfile
DELETED
data/gemfiles/rails_5.2.gemfile
DELETED