New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support custom checks against the TableDefinition object in the create_table
block
#170
Comments
Hey @andylouisqin, as far as I know, it should be fine to add a foreign key to a new table since there's no potentially expensive validation step. Are there other use cases you're thinking of for |
Hi @ankane, we thought the same thing, and got bitten by a table lock on a busy referenced table. It turns out that Postgres locks and scans the foreign referenced table column when adding a validated foreign key to a new table. Reading the Postgres Documentation for CREATE TABLE > REFERENCES
And now the important bit: a
Once the lock is created Postgres kicks off a table scan of the referenced table foreign key to verify that it is a valid foreign key. For reference, the following migration with a call to create_table(:menus) do |t|
t.references :restaurant, null: false, foreign_key: true
end BEGIN;
CREATE TABLE menus (
id bigserial primary key,
restaurants_id bigserial NOT NULL,
CONSTRAINT fk_rails_c0fe971f03
FOREIGN KEY (restaurants_id)
REFERENCES restaurants (id)
);
CREATE INDEX index_menus_on_restaurants_id ON menus (restaurants_id);
COMMIT;
|
It requires a lock, but I don't think it scans the table. CREATE TABLE restaurants (id bigserial primary key);
INSERT INTO restaurants SELECT i FROM generate_series(1, 10000000) i;
\timing on
BEGIN;
CREATE TABLE menus (
id bigserial primary key,
restaurants_id bigserial NOT NULL,
CONSTRAINT fk_rails_c0fe971f03
FOREIGN KEY (restaurants_id)
REFERENCES restaurants (id)
);
CREATE INDEX index_menus_on_restaurants_id ON menus (restaurants_id);
COMMIT; Output
Feel free to edit the script to reproduce the issue you saw. |
It requires a lock on the referenced table, but does not scan it, because there is no need to do so - the new table is empty. Did you configured |
Ok, just restating to make sure I understand:
Does this sound right?
I sure have set a |
Going back to @andylouisqin's point,
|
Yes, basically the process is as you described. You can read more here https://joinhandshake.com/blog/our-team/postgresql-and-lock-queue/ and https://github.com/tbicr/django-pg-zero-downtime-migrations#transactions-fifo-waiting
This is indeed dangerous, if Also a useful technique would be to use lock retries - https://postgres.ai/blog/20210923-zero-downtime-postgres-schema-migrations-lock-timeout-and-retries I saw Andrew have plans to support it.
Can you provide a specific example? |
Exactly! It isn't safe. Foreign keys should be added in separate statements to be safe, e.g. create_table :menus do |t|
t.references :restaurant, null: false
end
add_foreign_key :menus, :restaurants, validate: false
validate_foreign_key :menus, :restaurants
Cool! This is an excellent article.
This applies to any migration where the wrapping ddl transaction is disabled to support adding an index and a table is created or altered. Bad class CreateMenus < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def change
add_column :menus, :name, :text
add_index :menus, :restaurant_id, algorithm: :concurrently
end
end Good class CreateMenus < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def change
ActiveRecord::Base.transaction do
add_column :menus, :name, :text
end
add_index :menus, :restaurant_id, algorithm: :concurrently
end
end |
For new tables, there is no difference in this and adding a foreign key in
Explicitly calling If you have more questions or need clarifications, would be happy to answer. |
P.S. Thanks for taking all the time to provide feedback here. |
add_foreign_key :table1, :table2 This will acquire a Second: add_foreign_key :table1, :table2, validate: false # (1)
validate_foreign_key :table1, :table2 # (2) Both (1) and (2) acquire the same So as you can see it is better and with the same safety level is just run the "First" approach.
|
My point here is that I think that StrongMigrations could generate the same warning when creating a table.
|
Nope, because it is safe to add a foreign key to new tables if configured a |
Sure P.S. I am happy to look into adding this warning and opening a PR. I don't really want to draw this out any further. |
@bullfight Adding a foreign key is safe on an empty table and potentially unsafe on a non-empty table (writes are blocked while the foreign key is validated), which is why it warns in one situation but not the other. @andylouisqin If you have other ideas for checks involving the table definition, feel free to create a new issue. |
I don't know the exact differences but our teams understanding is the same as @bullfight's. That Postgres can lock the referenced tables so it's not safe. Here's an example migration that brought our db to a halt. # 1
create_table :x do |t|
t.references :busy_table_one, null: false, index: true,
foreign_key: {on_delete: :cascade}
t.references :from_busy_table_two, null: false, index: true,
foreign_key: {to_table: :busy_table_two, on_delete: :cascade}
t.references :to_busy_table_two, null: false, index: true
foreign_key: {to_table: :busy_table_two, on_delete: :cascade}
t.timestamps
end Adding / validating the foreign keys across separately did not. # 1
create_table :x do |t|
t.references :busy_table_one, null: false, index: true
t.references :from_busy_table_two, null: false, index: true
t.references :to_busy_table_two, null: false, index: true
t.timestamps
end
# 2
add_foreign_key :x, :busy_table_one, column: :busy_table_one_id,
validate: false, on_delete: :cascade
# 3
add_foreign_key :x, :busy_table_two, column: :from_busy_table_two_id,
validate: false, on_delete: :cascade
# 4
add_foreign_key :x, :busy_table_two, column: :to_busy_table_two_id,
validate: false, on_delete: :cascade
# 5
validate_foreign_key :x, column: :busy_table_one_id
validate_foreign_key :x, column: :from_busy_table_two_id
validate_foreign_key :x, column: :to_busy_table_two_id Just one example, so not conclusive. But we're now enforcing adding foreign keys separately because we've been bitten more than once by this. Keen to hear if you know what else could be the issue with the first migration. |
Adding a single foreign key when creating a table is safe, but adding multiple foreign keys is not, as you actually experienced. See https://github.com/fatkodima/online_migrations#adding-multiple-foreign-keys for the details. |
Aha! Thanks for the insight. Coming back to the original post, perhaps strong migrations should warn about this or allow custom checks for it.
|
Hi! There's currently a check against adding foreign key constraints unsafely. This problem is that this doesn't include adding foreign keys using the
create_table
method, e.g.:This circumvents the check, which can cause locking and downtime. Some options in the solution space here:
create_table
for foreign key additionsThe text was updated successfully, but these errors were encountered: