pg_ha_migrations 1.5.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5eb93231a1de0f12819682bfe42606bf0cc49813a50b91670107f01eafe8f18
4
- data.tar.gz: adcea5f591af66bb790118c562a0cab80dca009421529437d2586e6c0b957f01
3
+ metadata.gz: 9111677a3084d7b769a43f1c7078822c9ae84420443893f54dc1e228bfa037d1
4
+ data.tar.gz: 7f1db28ace4c6416980c1f5c3461669f43bd7cb116996c2af7759d48515a8899
5
5
  SHA512:
6
- metadata.gz: c7c827c5385b13a438a4a2cf1eaa1b7535e54e6d3293f67616849647706ca869a1362db4e102e66802f0930d450a09e3943d365a6b9d7481871c286c3fba1a36
7
- data.tar.gz: 334f6c562b0b1288690ff6ec1033beb6676446f4c3a5d708d65de93884e85a65a863d985a37bc02a0c30046d34fbd289f9ac5cca50ada30a9e6d4fcdc0d977ac
6
+ metadata.gz: 30c567438be90db49faf206696a51176ea397d448913194ac7b2de02349294cced3d3c94fc08e3b5225eb7b1678d997af716de4f3da9cf15491745ae92af2a22
7
+ data.tar.gz: dba6fa0e40b690a838a2ae26bd48a0cf18b0acda0922a74a14ae8dec295f1c3a48181a11f0ed4bc6cb46f24e9dadd03dcad86ec805c83c00ecd0cf998176cead
@@ -5,39 +5,32 @@ 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
+ ruby:
14
+ - 3.0
15
+ - 3.1
16
+ - 3.2
12
17
  gemfile:
13
- - rails_5.0
14
- - rails_5.1
15
- - rails_5.2
16
- - rails_6.0
17
18
  - rails_6.1
18
19
  - rails_7.0
19
- name: PostgreSQL ${{ matrix.pg }}
20
+ name: PostgreSQL ${{ matrix.pg }} - Ruby ${{ matrix.ruby }} - ${{ matrix.gemfile }}
20
21
  runs-on: ubuntu-latest
21
22
  env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
22
23
  BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
23
24
  ImageOS: ubuntu20
24
- services:
25
- postgresql:
26
- image: postgres:${{ matrix.pg }}
27
- env:
28
- POSTGRES_PASSWORD: postgres
29
- # Set health checks to wait until postgres has started
30
- options: >-
31
- --health-cmd pg_isready
32
- --health-interval 10s
33
- --health-timeout 5s
34
- --health-retries 5
35
- ports:
36
- - 5432:5432
37
25
  steps:
38
- - uses: actions/checkout@v2
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 }}
39
31
  - name: Setup Ruby using .ruby-version file
40
32
  uses: ruby/setup-ruby@v1
41
33
  with:
34
+ ruby-version: ${{ matrix.ruby }}
42
35
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
43
36
  - run: bundle exec rake spec
data/.pryrc CHANGED
@@ -1,9 +1,9 @@
1
- if defined?(PryByebug)
2
- Pry.commands.alias_command 'c', 'continue'
3
- Pry.commands.alias_command 's', 'step'
4
- Pry.commands.alias_command 'n', 'next'
5
- Pry.commands.alias_command 'f', 'finish'
6
- end
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-2.7
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.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://travis-ci.org/braintree/pg_ha_migrations.svg?branch=master)](https://travis-ci.org/braintree/pg_ha_migrations/)
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/braintree-product-technology/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680) on the [Braintree Product and Technology Blog](https://medium.com/braintree-product-technology). Many of the approaches we take and choices we've made are explained in much greater depth there than in this README.
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/braintree-product-technology/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680#360a), PgHaMigrations does not support ActiveRecord's automatic migration rollback capability.
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/braintree-product-technology/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.
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 `true`, raise an error when adding a column and separately setting a constant default value for that column in the same migration. Default: `false`
278
- - 'allow_force_create_table`: If false, the `force: true` option to ActiveRecord's `create_table` method is disallowed. Default: `true`
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 run -d -p 127.0.0.1:5432:5432 -e POSTGRES_PASSWORD="${PGPASSWORD}" postgres:${PGVERSION}
11
+ # Launch a blank postgres image with partman for testing
12
+ docker-compose up -d --build
@@ -0,0 +1,11 @@
1
+ version: "3.3"
2
+ services:
3
+ db:
4
+ build:
5
+ context: .
6
+ args:
7
+ - PGVERSION=${PGVERSION:-15}
8
+ ports:
9
+ - "5432:5432"
10
+ environment:
11
+ - POSTGRES_PASSWORD=${PGPASSWORD:-postgres}
@@ -2,6 +2,6 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rails", "7.0.0"
5
+ gem "rails", "7.0.1"
6
6
 
7
7
  gemspec path: "../"
@@ -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
@@ -65,7 +75,7 @@ module PgHaMigrations::SafeStatements
65
75
 
66
76
  def unsafe_add_column(table, column, type, options = {})
67
77
  safely_acquire_lock_for_table(table) do
68
- super(table, column, type, options)
78
+ super(table, column, type, **options)
69
79
  end
70
80
  end
71
81
 
@@ -145,7 +155,7 @@ module PgHaMigrations::SafeStatements
145
155
  end
146
156
 
147
157
  def safe_add_concurrent_index(table, columns, options={})
148
- unsafe_add_index(table, columns, options.merge(:algorithm => :concurrently))
158
+ unsafe_add_index(table, columns, **options.merge(:algorithm => :concurrently))
149
159
  end
150
160
 
151
161
  def safe_remove_concurrent_index(table, options={})
@@ -157,7 +167,7 @@ module PgHaMigrations::SafeStatements
157
167
  end
158
168
  index_size = select_value("SELECT pg_size_pretty(pg_relation_size('#{options[:name]}'))")
159
169
  say "Preparing to drop index #{options[:name]} which is #{index_size} on disk..."
160
- unsafe_remove_index(table, options.merge(:algorithm => :concurrently))
170
+ unsafe_remove_index(table, **options.merge(:algorithm => :concurrently))
161
171
  end
162
172
 
163
173
  def safe_set_maintenance_work_mem_gb(gigabytes)
@@ -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
@@ -19,6 +19,7 @@ module PgHaMigrations::UnsafeStatements
19
19
 
20
20
  execute_ancestor_statement(method_name, *args, &block)
21
21
  end
22
+ ruby2_keywords method_name
22
23
  end
23
24
 
24
25
  def self.delegate_unsafe_method_to_migration_base_class(method_name)
@@ -29,6 +30,7 @@ module PgHaMigrations::UnsafeStatements
29
30
 
30
31
  execute_ancestor_statement(method_name, *args, &block)
31
32
  end
33
+ ruby2_keywords "unsafe_#{method_name}"
32
34
  end
33
35
 
34
36
  delegate_unsafe_method_to_migration_base_class :add_column
@@ -43,6 +45,8 @@ module PgHaMigrations::UnsafeStatements
43
45
  delegate_unsafe_method_to_migration_base_class :remove_index
44
46
  delegate_unsafe_method_to_migration_base_class :add_foreign_key
45
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
46
50
 
47
51
  disable_or_delegate_default_method :create_table, ":create_table is NOT SAFE! Use safe_create_table instead"
48
52
  disable_or_delegate_default_method :add_column, ":add_column is NOT SAFE! Use safe_add_column instead"
@@ -59,13 +63,15 @@ module PgHaMigrations::UnsafeStatements
59
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"
60
64
  disable_or_delegate_default_method :add_foreign_key, ":add_foreign_key is NOT SAFE! Explicitly call :unsafe_add_foreign_key"
61
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"
62
68
 
63
69
  def unsafe_create_table(table, options={}, &block)
64
70
  if options[:force] && !PgHaMigrations.config.allow_force_create_table
65
71
  raise PgHaMigrations::UnsafeMigrationError.new(":force is NOT SAFE! Explicitly call unsafe_drop_table first if you want to recreate an existing table")
66
72
  end
67
73
 
68
- execute_ancestor_statement(:create_table, table, options, &block)
74
+ execute_ancestor_statement(:create_table, table, **options, &block)
69
75
  end
70
76
 
71
77
  def unsafe_add_index(table, column_names, options = {})
@@ -74,11 +80,10 @@ module PgHaMigrations::UnsafeStatements
74
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"
75
81
  end
76
82
 
77
- execute_ancestor_statement(:add_index, table, column_names, options)
83
+ execute_ancestor_statement(:add_index, table, column_names, **options)
78
84
  end
79
85
 
80
-
81
- def execute_ancestor_statement(method_name, *args, &block)
86
+ ruby2_keywords def execute_ancestor_statement(method_name, *args, &block)
82
87
  # Dispatching here is a bit complicated: we need to execute the method
83
88
  # belonging to the first member of the inheritance chain (besides
84
89
  # UnsafeStatements). If don't find the method in the inheritance chain,
@@ -1,3 +1,3 @@
1
1
  module PgHaMigrations
2
- VERSION = "1.5.0"
2
+ VERSION = "1.7.0"
3
3
  end
@@ -3,13 +3,15 @@ require "rails"
3
3
  require "active_record"
4
4
  require "active_record/migration"
5
5
  require "relation_to_struct"
6
+ require "ruby2_keywords"
6
7
 
7
8
  module PgHaMigrations
8
9
  Config = Struct.new(
9
10
  :disable_default_migration_methods,
10
11
  :check_for_dependent_objects,
11
12
  :allow_force_create_table,
12
- :prefer_single_step_column_addition_with_default
13
+ :prefer_single_step_column_addition_with_default,
14
+ :infer_primary_key_on_partitioned_tables,
13
15
  )
14
16
 
15
17
  def self.config
@@ -17,7 +19,8 @@ module PgHaMigrations
17
19
  true,
18
20
  false,
19
21
  true,
20
- false
22
+ false,
23
+ true
21
24
  )
22
25
  end
23
26
 
@@ -41,7 +44,7 @@ module PgHaMigrations
41
44
  # raise this error. For example, adding a column without a default and
42
45
  # then setting its default in a second action in a single migration
43
46
  # isn't our documented best practice and will raise this error.
44
- BestPracticeError = Class.new(Exception)
47
+ BestPracticeError = Class.new(StandardError)
45
48
 
46
49
  # Unsupported migrations use ActiveRecord::Migration features that
47
50
  # we don't support, and therefore will likely have unexpected behavior.
@@ -53,6 +56,7 @@ end
53
56
 
54
57
  require "pg_ha_migrations/blocking_database_transactions"
55
58
  require "pg_ha_migrations/blocking_database_transactions_reporter"
59
+ require "pg_ha_migrations/partman_config"
56
60
  require "pg_ha_migrations/unsafe_statements"
57
61
  require "pg_ha_migrations/safe_statements"
58
62
  require "pg_ha_migrations/dependent_objects_checks"
@@ -37,6 +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", ">= 5.0", "< 7.1"
40
+ spec.add_dependency "rails", ">= 6.1", "< 7.1"
41
41
  spec.add_dependency "relation_to_struct", ">= 1.5.1"
42
+ spec.add_dependency "ruby2_keywords"
42
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.5.0
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: 2022-05-09 00:00:00.000000000 Z
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: '5.0'
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: '5.0'
133
+ version: '6.1'
134
134
  - - "<"
135
135
  - !ruby/object:Gem::Version
136
136
  version: '7.1'
@@ -148,6 +148,20 @@ dependencies:
148
148
  - - ">="
149
149
  - !ruby/object:Gem::Version
150
150
  version: 1.5.1
151
+ - !ruby/object:Gem::Dependency
152
+ name: ruby2_keywords
153
+ requirement: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ type: :runtime
159
+ prerelease: false
160
+ version_requirements: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
151
165
  description: Enforces DDL/migration safety in Ruby on Rails project with an emphasis
152
166
  on explicitly choosing trade-offs and avoiding unnecessary magic.
153
167
  email:
@@ -163,17 +177,15 @@ files:
163
177
  - ".ruby-version"
164
178
  - Appraisals
165
179
  - CODE_OF_CONDUCT.md
180
+ - Dockerfile
166
181
  - Gemfile
167
182
  - LICENSE.txt
168
183
  - README.md
169
184
  - Rakefile
170
185
  - bin/console
171
186
  - bin/setup
187
+ - docker-compose.yml
172
188
  - gemfiles/.bundle/config
173
- - gemfiles/rails_5.0.gemfile
174
- - gemfiles/rails_5.1.gemfile
175
- - gemfiles/rails_5.2.gemfile
176
- - gemfiles/rails_6.0.gemfile
177
189
  - gemfiles/rails_6.1.gemfile
178
190
  - gemfiles/rails_7.0.gemfile
179
191
  - lib/pg_ha_migrations.rb
@@ -183,6 +195,7 @@ files:
183
195
  - lib/pg_ha_migrations/dependent_objects_checks.rb
184
196
  - lib/pg_ha_migrations/hacks/cleanup_unnecessary_output.rb
185
197
  - lib/pg_ha_migrations/hacks/disable_ddl_transaction.rb
198
+ - lib/pg_ha_migrations/partman_config.rb
186
199
  - lib/pg_ha_migrations/railtie.rb
187
200
  - lib/pg_ha_migrations/safe_statements.rb
188
201
  - lib/pg_ha_migrations/unsafe_statements.rb
@@ -208,7 +221,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
208
221
  - !ruby/object:Gem::Version
209
222
  version: '0'
210
223
  requirements: []
211
- rubygems_version: 3.1.4
224
+ rubygems_version: 3.2.3
212
225
  signing_key:
213
226
  specification_version: 4
214
227
  summary: Enforces DDL/migration safety in Ruby on Rails project with an emphasis on
@@ -1,7 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "rails", "5.0.7.2"
6
-
7
- gemspec path: "../"
@@ -1,7 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "rails", "5.1.7"
6
-
7
- gemspec path: "../"
@@ -1,7 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "rails", "5.2.3"
6
-
7
- gemspec path: "../"
@@ -1,7 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "rails", "6.0.0"
6
-
7
- gemspec path: "../"