-
Notifications
You must be signed in to change notification settings - Fork 21.4k
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
Verify foreign keys after loading fixtures #42674
Verify foreign keys after loading fixtures #42674
Conversation
activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
Outdated
Show resolved
Hide resolved
activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
Outdated
Show resolved
Hide resolved
e03ae36
to
fcb761c
Compare
20c2cdc
to
9f295e0
Compare
activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
Show resolved
Hide resolved
activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
Outdated
Show resolved
Hide resolved
When writing fixtures, it's currently possible to define associations that don't exist, even if a foreign key exists. For example: ```yml george: name: "Curious George" pirate: redbeard blackbeard: name: "Blackbeard" ``` When the fixtures are created, `parrots(:george).pirate` will be nil, but it's not immediately clear why. This can make it hard to debug tests and can give false confidence in passing ones. This can happen because Rails [disables referential integrity](https://github.com/rails/rails/blob/f263530bf709f611920b5f663882e5113b4f984b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb#L407) when inserting fixtures. This makes the fixtures algorithm much simpler - it can just create the fixtures in alphabetical order and assume that the other side of a foreign key constraint will *eventually* be added. Ideally we would check foreign keys once all fixtures have been loaded, so that we can be sure that the foreign key constraints were met. This PR introduces that. To enable it: ```ruby config.active_record.verify_foreign_keys_for_fixtures = true ``` I'm proposing we enable this in 7.0 for new apps and have added it to new framework defaults. When run against our app, it found 3 fixture files with unmet FK constraints - turns out all those fixtures weren't being used and were safe to delete.
9f295e0
to
47467fe
Compare
PSA: if anyone who knows more MySQL than me comes across this it’d be great to see it implemented for that too. I’m happy to help with the PR |
@ghiculescu If it's worth adding this to |
Yes. 🤦 I will tomorrow. |
…o_fk_violations Managed to reproduce CI failure at https://buildkite.com/rails/rails/builds/79496#0c03f856-9be1-4ca0-88c9-e1df21ae0a07 This commitaddresses the following errors by loading :author_addresses fixture because `:authors` has a foreign key to `:author_addresses`. * sqlite3 adapter ```ruby $ bin/test test/cases/adapters/sqlite3/explain_test.rb test/cases/fixtures_test.rb -n "/^(?:SQLite3ExplainTest#(?:test_explain_with_eager_loading)|FixturesWithForeignKeyViolationsTest#(?:test_does_not_raise_if_no_fk_violations))$/" --seed 2529 Using sqlite3 Run options: -n "/^(?:SQLite3ExplainTest#(?:test_explain_with_eager_loading)|FixturesWithForeignKeyViolationsTest#(?:test_does_not_raise_if_no_fk_violations))$/" --seed 2529 .E Error: FixturesWithForeignKeyViolationsTest#test_does_not_raise_if_no_fk_violations: RuntimeError: Foreign key violations found in your fixture data. Ensure you aren't referring to labels that don't exist on associations. /home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:641:in `block in insert' /home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:629:in `each' /home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:629:in `insert' /home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:615:in `read_and_insert' /home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:567:in `create_fixtures' /home/yahonda/src/github.com/rails/rails/activerecord/test/cases/fixtures_test.rb:844:in `block (2 levels) in test_does_not_raise_if_no_fk_violations' /home/yahonda/src/github.com/rails/rails/activesupport/lib/active_support/testing/assertions.rb:34:in `assert_nothing_raised' /home/yahonda/src/github.com/rails/rails/activerecord/test/cases/fixtures_test.rb:843:in `block in test_does_not_raise_if_no_fk_violations' /home/yahonda/src/github.com/rails/rails/activerecord/test/cases/fixtures_test.rb:857:in `with_verify_foreign_keys_for_fixtures' /home/yahonda/src/github.com/rails/rails/activerecord/test/cases/fixtures_test.rb:842:in `test_does_not_raise_if_no_fk_violations' bin/test test/cases/fixtures_test.rb:835 Finished in 0.045767s, 43.6991 runs/s, 174.7966 assertions/s. 2 runs, 8 assertions, 0 failures, 1 errors, 0 skips $ ``` * postgresql adapter ```ruby $ ARCONN=postgresql bin/test test/cases/adapters/postgresql/explain_test.rb test/cases/fixtures_test.rb -n "/^(?:PostgreSQLExplainTest#(?:test_explain_with_eager_loading)|FixturesWithForeignKeyViolationsTest#(?:test_does_not_raise_if_no_fk_violations))$/" --seed 16926 Using postgresql Run options: -n "/^(?:PostgreSQLExplainTest#(?:test_explain_with_eager_loading)|FixturesWithForeignKeyViolationsTest#(?:test_does_not_raise_if_no_fk_violations))$/" --seed 16926 .E Error: FixturesWithForeignKeyViolationsTest#test_does_not_raise_if_no_fk_violations: RuntimeError: Foreign key violations found in your fixture data. Ensure you aren't referring to labels that don't exist on associations. /home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:641:in `block in insert' /home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:629:in `each' /home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:629:in `insert' /home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:615:in `read_and_insert' /home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:567:in `create_fixtures' /home/yahonda/src/github.com/rails/rails/activerecord/test/cases/fixtures_test.rb:844:in `block (2 levels) in test_does_not_raise_if_no_fk_violations' /home/yahonda/src/github.com/rails/rails/activesupport/lib/active_support/testing/assertions.rb:34:in `assert_nothing_raised' /home/yahonda/src/github.com/rails/rails/activerecord/test/cases/fixtures_test.rb:843:in `block in test_does_not_raise_if_no_fk_violations' /home/yahonda/src/github.com/rails/rails/activerecord/test/cases/fixtures_test.rb:857:in `with_verify_foreign_keys_for_fixtures' /home/yahonda/src/github.com/rails/rails/activerecord/test/cases/fixtures_test.rb:842:in `test_does_not_raise_if_no_fk_violations' bin/test test/cases/fixtures_test.rb:835 Finished in 0.121193s, 16.5027 runs/s, 49.5080 assertions/s. 2 runs, 6 assertions, 0 failures, 1 errors, 0 skips $ ``` * It does not reproduce with mysql2 adapter because MySQL does not have features to validate existing foreign keys, but added :author_addresses in Mysql2ExplainTest for consistency. Follow up rails#42674
However, sometimes the process of fishing out the guilty party when one absolutely needs many fixtures becomes excessively long. Geometrically long, in fact. Instead, when this monster rears its head, I would instinctively like to do something akin to: |
@mjc-gh @dvodvo you might be interested in this PR that @danini-the-panini is working on: #44943 |
@ghiculescu cool feature! |
rails#42674 added the ability to have Rails verify foreign keys when creating fixtures. Feedback from users since then is it would be handy to know *which* foreign keys are being violated. See rails#44943 and rails#47780 for attempts to fix this. This PR rolls up some of the ideas from those PRs into one that's hopefully mergable. - [x] `all_foreign_keys_valid?` is deprecated in favour of `check_all_foreign_keys_valid!`. - [x] Postgres and Sqlite adapters raise an error with detail about the foreign key error. - [x] Authors of other PRs added as co-authors here to get credit. - [x] Deprecations updated to work with new deprecation APIs. - [x] Tests updated. Here's what the error messages will now look like. Postgres: ``` Foreign key violations found in your fixture data. Ensure you aren't referring to labels that don't exist on associations. Error from database: PG::ForeignKeyViolation: ERROR: insert or update on table "fk_pointing_to_non_existent_objects" violates foreign key constraint "fk_that_will_be_broken" DETAIL: Key (fk_object_to_point_to_id)=(980190962) is not present in table "fk_object_to_point_tos". CONTEXT: SQL statement "UPDATE pg_constraint SET convalidated=false WHERE conname = 'fk_that_will_be_broken' AND connamespace::regnamespace = 'public'::regnamespace; ALTER TABLE public.fk_pointing_to_non_existent_objects VALIDATE CONSTRAINT fk_that_will_be_broken;" PL/pgSQL function inline_code_block line 16 at EXECUTE ``` Sqlite: ``` Foreign key violations found in your fixture data. Ensure you aren't referring to labels that don't exist on associations. Error from database: Foreign key violations found: fk_pointing_to_non_existent_objects ``` Closes rails#47780 Closes rails#44943 Co-Authored-By: s-mage <s-mage@users.noreply.github.com> Co-Authored-By: danini-the-panini <danini-the-panini@users.noreply.github.com>
rails#42674 added the ability to have Rails verify foreign keys when creating fixtures. Feedback from users since then is it would be handy to know *which* foreign keys are being violated. See rails#44943 and rails#47780 for attempts to fix this. This PR rolls up some of the ideas from those PRs into one that's hopefully mergable. - [x] `all_foreign_keys_valid?` is deprecated in favour of `check_all_foreign_keys_valid!`. - [x] Postgres and Sqlite adapters raise an error with detail about the foreign key error. - [x] Authors of other PRs added as co-authors here to get credit. - [x] Deprecations updated to work with new deprecation APIs. - [x] Tests updated. Here's what the error messages will now look like. Postgres: ``` Foreign key violations found in your fixture data. Ensure you aren't referring to labels that don't exist on associations. Error from database: PG::ForeignKeyViolation: ERROR: insert or update on table "fk_pointing_to_non_existent_objects" violates foreign key constraint "fk_that_will_be_broken" DETAIL: Key (fk_object_to_point_to_id)=(980190962) is not present in table "fk_object_to_point_tos". CONTEXT: SQL statement "UPDATE pg_constraint SET convalidated=false WHERE conname = 'fk_that_will_be_broken' AND connamespace::regnamespace = 'public'::regnamespace; ALTER TABLE public.fk_pointing_to_non_existent_objects VALIDATE CONSTRAINT fk_that_will_be_broken;" PL/pgSQL function inline_code_block line 16 at EXECUTE ``` Sqlite: ``` Foreign key violations found in your fixture data. Ensure you aren't referring to labels that don't exist on associations. Error from database: Foreign key violations found: fk_pointing_to_non_existent_objects ``` Closes rails#47780 Closes rails#44943 Co-Authored-By: s-mage <s-mage@users.noreply.github.com> Co-Authored-By: danini-the-panini <danini-the-panini@users.noreply.github.com>
rails#42674 added the ability to have Rails verify foreign keys when creating fixtures. Feedback from users since then is it would be handy to know *which* foreign keys are being violated. See rails#44943 and rails#47780 for attempts to fix this. This PR rolls up some of the ideas from those PRs into one that's hopefully mergable. - [x] `all_foreign_keys_valid?` is deprecated in favour of `check_all_foreign_keys_valid!`. - [x] Postgres and Sqlite adapters raise an error with detail about the foreign key error. - [x] Authors of other PRs added as co-authors here to get credit. - [x] Deprecations updated to work with new deprecation APIs. - [x] Tests updated. Here's what the error messages will now look like. Postgres: ``` Foreign key violations found in your fixture data. Ensure you aren't referring to labels that don't exist on associations. Error from database: PG::ForeignKeyViolation: ERROR: insert or update on table "fk_pointing_to_non_existent_objects" violates foreign key constraint "fk_that_will_be_broken" DETAIL: Key (fk_object_to_point_to_id)=(980190962) is not present in table "fk_object_to_point_tos". CONTEXT: SQL statement "UPDATE pg_constraint SET convalidated=false WHERE conname = 'fk_that_will_be_broken' AND connamespace::regnamespace = 'public'::regnamespace; ALTER TABLE public.fk_pointing_to_non_existent_objects VALIDATE CONSTRAINT fk_that_will_be_broken;" PL/pgSQL function inline_code_block line 16 at EXECUTE ``` Sqlite: ``` Foreign key violations found in your fixture data. Ensure you aren't referring to labels that don't exist on associations. Error from database: Foreign key violations found: fk_pointing_to_non_existent_objects ``` Closes rails#47780 Closes rails#44943 Co-Authored-By: s-mage <s-mage@users.noreply.github.com> Co-Authored-By: danini-the-panini <danini-the-panini@users.noreply.github.com>
rails#42674 added the ability to have Rails verify foreign keys when creating fixtures. Feedback from users since then is it would be handy to know *which* foreign keys are being violated. See rails#44943 and rails#47780 for attempts to fix this. This PR rolls up some of the ideas from those PRs into one that's hopefully mergable. - [x] `all_foreign_keys_valid?` is deprecated in favour of `check_all_foreign_keys_valid!`. - [x] Postgres and Sqlite adapters raise an error with detail about the foreign key error. - [x] Authors of other PRs added as co-authors here to get credit. - [x] Deprecations updated to work with new deprecation APIs. - [x] Tests updated. Here's what the error messages will now look like. Postgres: ``` Foreign key violations found in your fixture data. Ensure you aren't referring to labels that don't exist on associations. Error from database: PG::ForeignKeyViolation: ERROR: insert or update on table "fk_pointing_to_non_existent_objects" violates foreign key constraint "fk_that_will_be_broken" DETAIL: Key (fk_object_to_point_to_id)=(980190962) is not present in table "fk_object_to_point_tos". CONTEXT: SQL statement "UPDATE pg_constraint SET convalidated=false WHERE conname = 'fk_that_will_be_broken' AND connamespace::regnamespace = 'public'::regnamespace; ALTER TABLE public.fk_pointing_to_non_existent_objects VALIDATE CONSTRAINT fk_that_will_be_broken;" PL/pgSQL function inline_code_block line 16 at EXECUTE ``` Sqlite: ``` Foreign key violations found in your fixture data. Ensure you aren't referring to labels that don't exist on associations. Error from database: Foreign key violations found: fk_pointing_to_non_existent_objects ``` Closes rails#47780 Closes rails#44943 Co-Authored-By: s-mage <s-mage@users.noreply.github.com> Co-Authored-By: danini-the-panini <danini-the-panini@users.noreply.github.com>
When writing fixtures, it's possible to define associations that don't exist, even if a foreign key exists. For example:
When the fixtures are created,
parrots(:george).pirate
will benil
, but it's not clear why. This can make it hard to debug tests and can give false confidence in passing ones.This happens because Rails disables referential integrity when inserting fixtures. This makes the fixtures algorithm much simpler - it can just create the fixtures in alphabetical order and assume that the other side of a foreign key constraint will eventually be added.
Ideally we would check foreign keys once all fixtures have been loaded, so that we can be sure that the foreign key constraints were met. This PR introduces that. To enable it:
I'm proposing we enable this in 7.0 for new apps and have added it to new framework defaults. When run against our app, it found 3 fixture files with unmet FK constraints - turns out all those fixtures weren't being used and were safe to delete.
The implementation works with SQLite (which has a builtin pragma for this) and PostgreSQL (which we use, and it was easy enough to write a query for). I haven't implemented for MySQL as I wasn't able to find any good advice on how to do it, but would love to add it in if anyone knows how.