Giter VIP home page Giter VIP logo

safe-pg-migrations's Introduction

safe-pg-migrations

ActiveRecord migrations for Postgres made safe.

safe-pg-migrations

Requirements

  • Ruby 3.0+
  • Rails 6.1+
  • PostgreSQL 11.7+

Usage

Just drop this line in your Gemfile:

gem 'safe-pg-migrations'

Note: Do not run migrations via PgBouncer connection if it is configured to use transactional or statement pooling modes. You must run migrations via a direct Postgres connection, or configure PgBouncer to use session pooling mode.

Example

Consider the following migration:

class AddPatientRefToAppointments < ActiveRecord::Migration[6.0]
  def change
    add_reference :appointments, :patient
  end
end

If the users table is large, running this migration will likely cause downtime. Safe PG Migrations hooks into Active Record so that the following gets executed instead:

class AddPatientRefToAppointments < ActiveRecord::Migration[6.0]
  # Do not wrap the migration in a transaction so that locks are held for a shorter time.
  disable_ddl_transaction!

  def change
    # Lower Postgres' lock timeout to avoid statement queueing. Acts like a seatbelt.
    execute("SET lock_timeout TO '5s'")

    # Lower Postgres' statement timeout to avoid too long transactions. Acts like a seatbelt.
    execute("SET statement_timeout TO '5s'")
    add_column :appointments, :patient_id, :bigint

    # add_index using the concurrent algorithm, to avoid locking the tables
    add_index :appointments, :patient_id, algorithm: :concurrently

    # add_foreign_key without validation, to avoid locking the table for too long
    execute("SET statement_timeout TO '5s'")
    add_foreign_key :appointments, :patients, validate: false

    execute("SET statement_timeout TO '0'")

    # validate the foreign key separately, it avoids taking a lock on the entire tables
    validate_foreign_key :appointments, :patients
    
    # we also need to set timeouts to their initial values if needed
  end
end

Under the hood, Safe PG Migrations patches ActiveRecord::Migration and extends ActiveRecord::Base.connection to make potentially dangerous methods—like add_reference—safe.

Motivation

Writing a safe migration can be daunting. Numerous articles, including ours, have been written on the topic and a few gems are trying to address the problem. Even for someone who has a pretty good command of Postgres, remembering all the subtleties of explicit locking is not a piece of cake.

Active Record means developers don't have to be proficient in SQL to interact with a database. In the same way, Safe PG Migrations was created so that developers don't have to understand the ins and outs of Postgres to write a safe migration.

Feature set

Lock timeout

Most DDL operations (e.g. adding a column, removing a column or adding a default value to a column) take an ACCESS EXCLUSIVE lock on the table they are altering. While these operations wait to acquire their lock, other statements are blocked. Before running a migration, Safe PG Migrations sets a short lock timeout (default to 5 seconds) so that statements are not blocked for too long.

See PostgreSQL Alter Table and Long Transactions and Migrations and Long Transactions for detailed explanations of the matter.

Statement timeout

Adding a foreign key or a not-null constraint can take a lot of time on a large table. The problem is that those operations take ACCESS EXCLUSIVE locks. We clearly don't want them to hold these locks for too long. Thus, Safe PG Migrations runs them with a short statement timeout (default to 5 seconds).

See Zero-downtime Postgres migrations - the hard parts for a detailed explanation on the subject.

Prevent wrapping migrations in transaction

When Safe PG Migrations is used, migrations are not wrapped in a transaction. This is for several reasons:

  • We want to release locks as soon as possible.
  • In order to be able to retry statements that have failed because of a lock timeout, we have to be outside a transaction.
  • In order to add an index concurrently, we have to be outside a transaction.

Note that if a migration fails, it won't be rolled back. This can result in migrations being partially applied. In that case, they need to be manually reverted.

Safe add_column

Safe PG Migrations gracefully handle the upgrade to PG11 by not backfilling default value for existing rows, as the database engine is now natively handling it.

Beware though, when adding a volatile default value:

add_column :users, :created_at, default: 'clock_timestamp()'

PG will still needs to update every row of the table, and will most likely statement timeout for big table. In this case, Safe PG Migrations can automatically backfill data when the option default_value_backfill: is set to :update_in_batches.

Safe add_column - adding a volatile default value

Safe PG Migrations provides the extra option parameter default_value_backfill:. When your migration is adding a volatile default value, the option :update_in_batches can be set. It will automatically backfill the value in a safe manner.

safety_assured do
  add_column :users, :created_at, default: 'clock_timestamp()', default_value_backfill: :update_in_batches
end

More specifically, it will:

  1. create the column without default value and without null constraint. This ensure the ACCESS EXCLUSIVE lock is acquired for the least amount of time;
  2. add the default value, without data backfill. An ACCESS EXCLUSIVE lock is acquired and released immediately;
  3. backfill data, in batch of SafePgMigrations.config.backfill_batch_size and with a pause of SafePgMigrations.config.backfill_pause between each batch;
  4. change the column to null: false, if defined in the parameters, following the algorithm we have defined below.

NOTE

Data backfill take time. If your table is big, your migrations will (safely) hangs for a while. You might want to backfill data manually instead, to do so you will need two migrations

  1. First migration :

    a. adds the column without default and without null constraint;

    b. add the default value.

  2. manual data backfill (rake task, manual operation, ...)

  3. Second migration which change the column to null false (with Safe PG Migrations, change_column_null is safe and can be used; see section below)


default_value_backfill: also accept the value :auto which is set by default. In this case, Safe PG Migrations will not backfill data and will let PostgreSQL handle it itself.

Preventing :update_in_batches when the table is too big

add_column with default_value_backfill: :update_in_batches can be dangerous on big tables. To avoid unwanted long migrations, Safe PG Migrations does not automatically mark this usage as safe when used with strong-migrations, usage of safety_assured is required.

It is also possible to set a threshold for the table size, above which the migration will fail. This can be done by setting the default_value_backfill_threshold: option in the configuration.

Safe add_index and remove_index

Creating an index requires a SHARE lock on the target table which blocks all write on the table while the index is created (which can take some time on a large table). This is usually not practical in a live environment. Thus, Safe PG Migrations ensures indexes are created concurrently.

As CREATE INDEX CONCURRENTLY and DROP INDEX CONCURRENTLY are non-blocking operations (ie: read/write operations on the table are still possible), Safe PG Migrations sets a lock timeout to 30 seconds for those 2 specific statements.

If you still get lock timeout while adding / removing indexes, it might be for one of those reasons:

  • Long-running queries are active on the table. To create / remove an index, PG needs to wait for the queries that are actually running to finish before starting the index creation / removal. The blocking activity logger might help you to pinpoint the culprit queries.
  • A vacuum / autovacuum is running on the table, holding a ShareUpdateExclusiveLock, you are most likely out of luck for the current migration, but you may try to optimize your autovacuums settings.
Safe add_foreign_key (and add_reference)

Adding a foreign key requires a SHARE ROW EXCLUSIVE lock, which prevent writing in the tables while the migration is running.

Adding the constraint itself is rather fast, the major part of the time is spent on validating this constraint. Thus Safe PG Migrations ensures that adding a foreign key holds blocking locks for the least amount of time by splitting the foreign key creation in two steps:

  1. adding the constraint without validation, will not validate existing rows;
  2. validating the constraint, will validate existing rows in the table, without blocking read or write on the table
Safe add_check_constraint (ActiveRecord > 6.1)

Adding a check constraint requires an ACCESS EXCLUSIVE lock, which prevent writing and reading in the tables as soon as the lock is requested.

Adding the constraint itself is rather fast, the major part of the time is spent on validating this constraint. Thus Safe PG Migrations ensures that adding a constraints holds blocking locks for the least amount of time by splitting the constraint addition in two steps:

  1. adding the constraint without validation, will not validate existing rows;
  2. validating the constraint, will validate existing rows in the table, without blocking read or write on the table
Safe change_column_null (ActiveRecord and PG version dependant)

Changing the nullability of a column requires an ACCESS EXCLUSIVE lock, which prevent writing and reading in the tables as soon as the lock is requested.

Adding the constraint itself is rather fast, the major part of the time is spent on validating this constraint.

Safe PG Migrations acts differently depending on the version you are on.

Recent versions of PG and Active Record (> 12 and > 6.1)

Starting on PostgreSQL versions 12, adding the column NOT NULL constraint is safe if a check constraint validates the nullability of the same column. Safe PG Migrations also relies on add_check_constraint, which was introduced in ActiveRecord 6.1.

If these requirements are met, Safe PG Migrations ensures that adding a constraints holds blocking locks for the least amount of time by splitting the constraint addition in several steps:

  1. adding a IS NOT NULL constraint without validation, will not validate existing rows but block read or write;
  2. validating the constraint, will validate existing rows in the table, without blocking read or write on the table;
  3. changing the not null status of the column, thanks to the NOT NULL constraint without having to scan the table sequentially;
  4. dropping the IS NOT NULL constraint.

Older versions of PG or ActiveRecord

If the version of PostgreSQL is below 12, or if the version of ActiveRecord is below 6.1, Safe PG Migrations will only wrap ActiveRecord method into a statement timeout and lock timeout.

Call with a default parameter

Calling change_column_null with a default parameter is dangerous and is likely not to finish in the statement timeout defined by Safe PG Migrations. For this reason, when the default parameter is given, Safe PG Migrations will simply forward it to activerecord methods without trying to improve it

Dropping a NULL constraint

Dropping a null constraint still requires an ACCESS EXCLUSIVE lock, but does not require extra operation to reduce the amount of time during which the lock is held. Safe PG Migrations only wrap methods of activerecord in lock and statement timeouts

Retry after lock timeout

When a statement fails with a lock timeout, Safe PG Migrations retries it (5 times max) list of retriable statements

Blocking activity logging

If a statement fails with a lock timeout, Safe PG Migrations will try to tell you what was the blocking statement.


NOTE

Data logged by the Blocking activity logger can be sensitive (it will contain raw SQL queries, which can be hashes of password, user information, ...)

If you cannot afford to log this type of data, you can either

  • Set SafePgMigrations.config.blocking_activity_logger_verbose = false. In this case, the logger will only log the pid of the blocking statement, which should be enough to investigate;
  • Provide a different logger through SafePgMigrations.config.sensitive_logger = YourLogger.new. Instead of using the default IO stream, SafePgMigrations will send sensitive data to the given logger which can be managed as you wish.

Dropping a table

Dropping a table can be difficult to achieve in a small amount of time if it holds several foreign keys to busy tables. To remove the table, PostgreSQL will have to acquire an access exclusive lock on all the tables referenced by the foreign keys.

To solve this issue, Safe Pg Migrations will drop the foreign keys before dropping the table.


NOTE

Dropping a table is a dangerous operation by nature. Safe Pg Migrations will not prevent the deletion of a table which would still be in use.


Verbose SQL logging

For any operation, Safe PG Migrations can output the performed SQL queries. This feature is enabled by default in a production Rails environment. If you want to explicit enable it, for example in your development environment you can use:

export SAFE_PG_MIGRATIONS_VERBOSE=1

Instead of the traditional output:

add_index :users, :age

== 20191215132355 SampleIndex: migrating ======================================
-- add_index(:users, :age)
   -> add_index("users", :age, {:algorithm=>:concurrently})
   -> 0.0175s
== 20191215132355 SampleIndex: migrated (0.0200s) =============================

Safe PG Migrations will output the following logs:

add_index :users, :age

== 20191215132355 SampleIndex: migrating ======================================
   (0.3ms)  SHOW lock_timeout
   (0.3ms)  SET lock_timeout TO '5s'
-- add_index(:users, :age)
   -> add_index("users", :age, {:algorithm=>:concurrently})
   (0.3ms)  SHOW statement_timeout
   (0.3ms)  SET statement_timeout TO 0
   (0.3ms)  SHOW lock_timeout
   (0.3ms)  SET lock_timeout TO '30s'
   (3.5ms)  CREATE INDEX CONCURRENTLY "index_users_on_age" ON "users"  ("age")
   (0.3ms)  SET lock_timeout TO '5s'
   (0.2ms)  SET statement_timeout TO '1min'
   -> 0.0093s
   (0.2ms)  SET lock_timeout TO '0'
== 20191215132355 SampleIndex: migrated (0.0114s) =============================

So you can actually check that the CREATE INDEX statement will be performed concurrently, without any statement timeout and with a lock timeout of 30 seconds.

Nb: The SHOW statements are used by Safe PG Migrations to query settings for their original values in order to restore them after the work is done

Configuration

Safe PG Migrations can be customized, here is an example of a Rails initializer (the values are the default ones):

SafePgMigrations.config.safe_timeout = 5.seconds # Statement timeout used for all DDL operations except from CREATE / DROP INDEX

SafePgMigrations.config.lock_timeout = nil # Lock timeout used for all DDL operations except from CREATE / DROP INDEX. If not set, safe_timeout will be used with a deduction of 1% to ensure that the lock timeout is raised in priority

SafePgMigrations.config.blocking_activity_logger_verbose = true # Outputs the raw blocking queries on timeout. When false, outputs information about the lock instead

SafePgMigrations.config.sensitive_logger = nil # When given, sensitive data will be sent to this logger instead of the standard output. Must implement method `info`.

SafePgMigrations.config.blocking_activity_logger_margin = 1.second # Delay to output blocking queries before timeout. Must be shorter than safe_timeout

SafePgMigrations.config.backfill_batch_size = 100_000 # Size of the batches used for backfilling when adding a column with a default value

SafePgMigrations.config.backfill_pause = 0.5.second # Delay between each batch during a backfill. This ensure replication can happen safely. 

SafePgMigrations.config.default_value_backfill_threshold = nil # When set, batch backfill will only be available if the table is under the given threshold. If the number of rows is higher (according to stats), the migration will fail. 

SafePgMigrations.config.retry_delay = 1.minute # Delay between retries for retryable statements

SafePgMigrations.config.max_tries = 5 # Number of retries before abortion of the migration

Authors

License

MIT © Doctolib

Additional resources

Alternatives:

Interesting reads:

safe-pg-migrations's People

Contributors

bjorand avatar bpaquet avatar bperbos avatar charlesdelannoy avatar charlottefeather avatar coisnepe avatar dependabot[bot] avatar izzette avatar katzenbar avatar matthieuprat avatar maximerety avatar mouadhilali avatar mristau-wm avatar pirj avatar rchoquet avatar sunny avatar tanguy-platsec avatar teckwan avatar tfe-github-3[bot] avatar thhareau avatar yann-texier avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

safe-pg-migrations's Issues

Can't rollback an add_index migration

This gem is a fantastic idea! Thank you for the work to make it available to the community.

I'm attempting to use it in a project, and ran into an issue straight away.

$ bundle info safe-pg-migrations
  * safe-pg-migrations (1.2.3)
	Summary: Make your PG migrations safe.
	Homepage: https://github.com/doctolib/safe-pg-migrations
	Path: /Users/ylansegal/.asdf/installs/ruby/3.0.1/lib/ruby/gems/3.0.0/gems/safe-pg-migrations-1.2.3

Here is my test migration:

class AddTestIndex < ActiveRecord::Migration[6.1]
  def change
    add_index :users, :created_at
  end
end

I can run db:migrate and see the expected safe operations for concurrently:

$ rails db:migrate
== 20210617192601 AddTestIndex: migrating =====================================
-- add_index(:users, :created_at)
   -> add_index("users", :created_at, {:algorithm=>:concurrently})
   -> 0.0087s
== 20210617192601 AddTestIndex: migrated (0.0118s) ============================

However, I encounter an error when I try to rollback:

$ rails db:rollback
== 20210617192601 AddTestIndex: reverting =====================================
-- remove_index(:users, :created_at)
   -> remove_index("users", {:column=>:created_at, :algorithm=>:concurrently})
rails aborted!
StandardError: An error has occurred, all later migrations canceled:

No indexes found on users with the options provided.

Caused by:
ArgumentError: No indexes found on users with the options provided.

Thank you!

Wrong number of arguments - Rails 6.1

When using with Rails 6.1 I receive a lot of times error:

StandardError: An error has occurred, all later migrations canceled:

wrong number of arguments (given 4, expected 3)

Migration:

add_column :actions, :approval_level_id, :bigint, index: true

Is this gem compatible with latest Rails release?

Option to disable IdempotentStatements?

Would you be open to adding an option to disable IdempotentStatements?

Idempotency is great, but the current code doesn't go quite far enough currently to ensure actual idempotence imo. E.g. it only verifies that a column name exists, not that it matches the column being requested. I know I've made mistakes along those lines in the past, and I'd rather get an error message instead of having it be ignored.

Are you using the idempotency approach in general to lessen the effect of disabling transactional migrations? Are there other aspects I'm not thinking about?

blocking_activity_logger: can't convert String into an exact number

rescue ActiveRecord::LockWaitTimeout
SafePgMigrations.say 'Lock timeout.', true
queries =
begin
blocking_queries_retriever_thread.value
rescue StandardError => e
SafePgMigrations.say("Error while retrieving blocking queries: #{e}", true)
nil
end
raise if queries.nil?
if queries.empty?
SafePgMigrations.say 'Could not find any blocking query.', true
else
SafePgMigrations.say(
"Statement was being blocked by the following #{'query'.pluralize(queries.size)}:", true
)
SafePgMigrations.say '', true
output_blocking_queries(queries)

currently debugging locally a (probably gem versioning) issue where this value has a string timestamp like:

[["select pg_sleep(300), id from table limit 1;", "2021-09-27 14:58:18.677782+00"]]

and then the time formatter blows up with:

can't convert String into an exact number
--
 0: /home/me/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/activesupport-5.2.6/lib/active_support/core_ext/time/calculations.rb:275:in `-'
 1: /home/me/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/activesupport-5.2.6/lib/active_support/core_ext/time/calculations.rb:275:in `minus_with_duration'
 2: /home/me/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/activesupport-5.2.6/lib/active_support/core_ext/time/calculations.rb:286:in `minus_with_coercion'
 3: /home/me/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/safe-pg-migrations-1.2.3/lib/safe-pg-migrations/plugins/blocking_activity_logger.rb:115:in `format_start_time'
 4: /home/me/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/safe-pg-migrations-1.2.3/lib/safe-pg-migrations/plugins/blocking_activity_logger.rb:100:in `block in output_blocking_queries'
 5: /home/me/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/safe-pg-migrations-1.2.3/lib/safe-pg-migrations/plugins/blocking_activity_logger.rb:100:in `each'
 6: /home/me/.rbenv/versions/2.7.3/lib/ruby/gems/2.7.0/gems/safe-pg-migrations-1.2.3/lib/safe-pg-migrations/plugins/blocking_activity_logger.rb:100:in `output_blocking_queries'
 [...]

`SET` and `disable_ddl_transaction!`

Those two don't play too well together when used via PgBouncer in transactional pooling mode.
Transactional pooling mode is the default for server-side (Postgres-side) PgBouncer installation on Heroku.

SET statement_timeout/CREATE INDEX CONCURRENTLY and reset of statement_timeout could run in three different server connections. Because of disable_ddl_transaction!. There are two problems here:

  • unsettling statement_timeout has no effect on CREATE INDEX CONCURRENTLY's timeout if they are executed in different server connections
  • we leave a connection hanging in PgBouncer's pool with unset statement_timeout

It makes sense to mention that somewhere in the documentation.

I can handle sending a PR if you confirm and approve the problem.

Version 2.2.1+ not allowing remove_column to contain 4 arguments

Steps to reproduce:

  • Set safe-pg-migrations to 2.2.1+ version
  • In a migration do a safety_assured and within the block call a remove_column with 4 arguments.
    e.g:
safety_assured do 
  remove_column :users, :first_name, :boolean, null: false, default: false
end

the 4th arguments is a hash containing null: false and default: false

error:

rake aborted!
StandardError: An error has occurred, all later migrations canceled:

wrong number of arguments (given 4, expected 2..3)

I've been looking at recent commits that were supplied, but wasn't able to find in which one this issue was introduced.

question: Is there a way to disable logging for some migrations?

We chose to go with this gem over strong-migrations because of the nicer logging. However if does not play well with a Gem called data-migrate. In the sense that the logging is really verbose, where updating thousands of records will create tens of thousands of migration logs.

So is there a way to disable the logging for some migrations but not others perhaps a class method or option ?

Idea: only disable DDL transaction for transactions that use helpers

That is practically achievable by providing a SafeMigration concern that users could include in their migrations.
It will:

  • provide updated helpers
  • set lock and statement timeouts
  • disable DDL transaction

This approach would still allow having migrations that have DDL transaction turned on.

safe rename_column seems to be missing?

Rename column is missing from the safe rails methods, is this intended?

SAFE_METHODS = %i[
execute
add_column
add_index
add_reference
add_belongs_to
change_column_null
add_foreign_key
].freeze

I ask because the alter table statement to rename a column will need an access exclusive lock, no?

rails also drops and recreates the index, using add_index which is a safe method.

https://github.com/rails/rails/blob/18da7b6ba71c5ae2ed212e9f3a9e8f6dd0732b12/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#L462-L466

Based on all of this, but I could be missing something, I would expect this gem to protect rename_column.

Error undefined method `table' for "index_name":String on v1.4.0

Hi, thanks for creating an amazing gem.

I ran into an issue on the recent 1.4.0 update, getting an error undefined method 'table' for "index_name":String when running a create table migration with reference or belongs_to
I tested It on 1.3.0 and it works fine

My env:
ruby 2.7.4
rails 5.2.5

Sample migration

class CreateComments < ActiveRecord::Migration[5.2]
  def change
    create_table :comments do |t|
      t.references :user, foreign_key: true
      t.string :text, null: false

      t.timestamps
    end
  end
end

Logs:

Caused by:
NoMethodError: undefined method `table' for "index_comments_on_user_id":String
Did you mean?  tableize
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/idempotent_statements.rb:10:in `add_index'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/statement_insurer.rb:76:in `block in add_index'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/statement_insurer.rb:121:in `block (2 levels) in without_timeout'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/statement_insurer.rb:117:in `block in without_lock_timeout'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/statement_insurer.rb:100:in `with_setting'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/statement_insurer.rb:117:in `without_lock_timeout'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/statement_insurer.rb:121:in `block in without_timeout'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/statement_insurer.rb:113:in `block in without_statement_timeout'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/statement_insurer.rb:100:in `with_setting'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/statement_insurer.rb:113:in `without_statement_timeout'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/statement_insurer.rb:121:in `without_timeout'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/statement_insurer.rb:76:in `add_index'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/useless_statements_logger.rb:15:in `add_index'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/connection_adapters/abstract/schema_statements.rb:315:in `block in create_table'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/connection_adapters/abstract/schema_statements.rb:314:in `each'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/connection_adapters/abstract/schema_statements.rb:314:in `create_table'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/blocking_activity_logger.rb:22:in `block (3 levels) in <module:BlockingActivityLogger>'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/blocking_activity_logger.rb:65:in `log_blocking_queries'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/blocking_activity_logger.rb:22:in `block (2 levels) in <module:BlockingActivityLogger>'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/idempotent_statements.rb:58:in `create_table'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/statement_insurer.rb:57:in `block in create_table'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/statement_insurer.rb:100:in `with_setting'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/statement_insurer.rb:56:in `create_table'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:871:in `block in method_missing'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:840:in `block in say_with_time'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:840:in `say_with_time'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:860:in `method_missing'
/Users/hassan/moodpath-rails/db/migrate/20220227212107_create_comments.rb:3:in `change'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:814:in `exec_migration'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/base.rb:81:in `block in exec_migration'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/base.rb:34:in `block in setup_and_teardown'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/plugins/statement_insurer.rb:100:in `with_setting'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/base.rb:34:in `setup_and_teardown'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/safe-pg-migrations-1.4.0/lib/safe-pg-migrations/base.rb:80:in `exec_migration'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:798:in `block (2 levels) in migrate'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:797:in `block in migrate'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/connection_adapters/abstract/connection_pool.rb:416:in `with_connection'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:796:in `migrate'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:977:in `migrate'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:1292:in `block in execute_migration_in_transaction'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:1345:in `ddl_transaction'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:1291:in `execute_migration_in_transaction'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:1263:in `block in migrate_without_lock'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:1262:in `each'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:1262:in `migrate_without_lock'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:1210:in `block in migrate'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:1363:in `with_advisory_lock'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:1210:in `migrate'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:1036:in `up'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/migration.rb:1011:in `migrate'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/tasks/database_tasks.rb:172:in `migrate'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activerecord-5.2.5/lib/active_record/railties/databases.rake:60:in `block (2 levels) in <top (required)>'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activesupport-5.2.5/lib/active_support/dependencies.rb:285:in `load'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activesupport-5.2.5/lib/active_support/dependencies.rb:285:in `block in load'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activesupport-5.2.5/lib/active_support/dependencies.rb:257:in `load_dependency'
/Users/hassan/.rvm/gems/ruby-2.7.4@moodpath-rails/gems/activesupport-5.2.5/lib/active_support/dependencies.rb:285:in `load'

Support default value on a table where id is a UUID

Feature request

Current state

safe-pg-migrations doesn't support to add a default value on table with a UUID primary key.

If you do, you will get the following error:

PG::UndefinedFunction: ERROR:  operator does not exist: uuid > integer
LINE 1: SELECT id FROM "mytable" WHERE id > 0 ORDER BY id LIMIT 10...

This issue is related to this line.

What is the expected behavior?

That safe-pg-migrations supports both integer and uuid for primary key id.

add_column, null: false should be executed in one statement

Such a migration

class AddAwesomeNotesToPatient
    def change
        add_column :awesome_notes, :string, array: true, default: [],  null: false
    end
end

is using two alter table statements whereas it should only use one.

image

The ideal statement should be

ALTER TABLE "patients" ADD "awesome_notes" character varying[] DEFAULT '{}' NOT NULL

Adding a foreign key causes `ArgumentError: unknown keyword: :column`

Hello 👋

First of all thank you for sharing this very handy gem which works out of the box and solves such a great pain 🙌

Problem
I ran into an issue using the latest 1.3.0 version of the gem on a ruby 2.7.4 rails 5.2.6 project.
Adding a foreign key to a table with add_reference triggered an ArgumentError: unknown keyword: :column when running rails db:migrate. I reproduced the error on a vanilla rails new project with the same versions to let you reproduce the 🐛 easily 👌
Here is the backtrace 👇

== 20211015145629 AddRestaurantReferenceToReviews: migrating ==================
-- add_reference(:reviews, :restaurant, {:foreign_key=>true})
   -> add_index("reviews", ["restaurant_id"], {:algorithm=>:concurrently})
rails aborted!
StandardError: An error has occurred, all later migrations canceled:

unknown keyword: :column
/Users/edwardschults/code/Eschults/safe-pg-add-reference/db/migrate/20211015145629_add_restaurant_reference_to_reviews.rb:3:in `change'
/Users/edwardschults/code/Eschults/safe-pg-add-reference/bin/rails:9:in `<top (required)>'
/Users/edwardschults/code/Eschults/safe-pg-add-reference/bin/spring:15:in `<top (required)>'
./bin/rails:3:in `load'
./bin/rails:3:in `<main>'

Caused by:
ArgumentError: unknown keyword: :column
/Users/edwardschults/code/Eschults/safe-pg-add-reference/db/migrate/20211015145629_add_restaurant_reference_to_reviews.rb:3:in `change'
/Users/edwardschults/code/Eschults/safe-pg-add-reference/bin/rails:9:in `<top (required)>'
/Users/edwardschults/code/Eschults/safe-pg-add-reference/bin/spring:15:in `<top (required)>'
./bin/rails:3:in `load'
./bin/rails:3:in `<main>'
Tasks: TOP => db:migrate
(See full trace by running task with --trace)

After debugging and step-ping in the source code, I found out that the error was coming from this line, which changed between 1.2.3 and 1.3.0 with the addition of support for Ruby 3.0.

Note that when this error occurs, the creation of the column actually goes through, but not the creation of the schema_migrations record, making any new rails db:migrate run into the error again and preventing next migrations to run.

Solution
Downgrading to 1.2.3 solves the error on ruby 2.7 👌

Cheers!

Escape hatch for unsafe migrations

Hi!

Some migrations unfortunately cannot be run safely and are then impossible to run.
When using a partitioned table on PostgreSQL, this migration, where slots is the partitioned table, will fail because of 2 Postgres errors:

    add_reference :slots, :variation, index: true, foreign_key: true
  1. ActiveRecord::StatementInvalid: PG::WrongObjectType: ERROR: cannot add NOT VALID foreign key on partitioned table "slots" referencing relation "variations": since validate: false is forced
  2. PG::FeatureNotSupported: ERROR: cannot create index on partitioned table "stock_slots" concurrently : since algorithm: :concurrently is forced

Is there a way to explicitly disable the safeness in a block? If not, what about a block:

  unsafe_migration do
    add_reference :slots, :variation, index: true, foreign_key: true, validate: true
  end

Telling: I know it is not safe, and then it can be caught before deployment to take appropriate measures.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.