-
Notifications
You must be signed in to change notification settings - Fork 21.9k
Add ability to create/validate invalid foreign keys in Postgres #27756
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
Conversation
48de907
to
bfc6ed0
Compare
Please advise on any places to add test coverage. I was having a bit of difficulty identifying where to add tests for certain things. |
40edbdb
to
0270b8d
Compare
@maclover7 is there a process I should be following for getting some 👀 on this PR? I believe I have some spec failures due to the |
@travisofthenorth I added the |
7dfe4ab
to
c396849
Compare
Sorry don't have the Postgres experience to vet this. @kamipo you've been doing wonders with Active Record, are you interested in giving this PR a review? 😊 |
So I prefer the naming here:
|
f9e6099
to
b458c77
Compare
@kamipo updated. A couple things to mention:
|
Ping @kamipo. Any other feedback? |
private | ||
def visit_AddForeignKey(o) | ||
super.tap { |sql| sql << " NOT VALID" if o.novalidate? } | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SchemaCreation
was extracted to lib/active_record/connection_adapters/postgresql/schema_creation.rb
.
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
index e1d5089115..afb51a92d3 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
@@ -3,6 +3,10 @@ module ConnectionAdapters
module PostgreSQL
class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc:
private
+ def visit_AddForeignKey(o)
+ super.tap { |sql| sql << " NOT VALID" if o.novalidate? }
+ end
+
def add_column_options!(sql, options)
if options[:collation]
sql << " COLLATE \"#{options[:collation]}\""
end | ||
|
||
def novalidate? | ||
options[:valid] == false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If an adapter doesn't support novalidate constraint, these valid?
and novalidate?
returns incorrect result.
How about it?
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 4332318a8e..5127d6bbe9 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -58,12 +58,8 @@ def custom_primary_key?
options[:primary_key] != default_primary_key
end
- def valid?
- options[:valid] == true
- end
-
- def novalidate?
- options[:valid] == false
+ def validate?
+ options.fetch(:validate, true)
end
def defined_for?(to_table_ord = nil, to_table: nil, **options)
d8b6275
to
60976c9
Compare
K, thanks for letting me know. Sorry for the noise! |
2868910
to
3be4619
Compare
Rebased on master and resolved a conflict. |
I'm a bit worried about how much stuff we're adding to the abstract layer, for a feature that only Postgres supports. Is any of that avoidable? 😕 |
I would like to see Rails promote more best practices in regards to database use. Foreign key and other constraints are a huge part of this. On one hand it is catering to a specific database. On the other hand, should users of that DB get a worse experience just because ALL databases don't support that feature? I know it's not covered in this PR but validation race conditions are a huge issue. Recently I had a validation cause over 80% of ALL load on my postgres database, and I had no idea. Here's the PR explaining the issue and fixing it codetriage/CodeTriage#573. It would like to see Rails continue to play better with postgres in the future. @matthewd RE: size. Half of the PR is tests. I agree that we want to keep the abstract layer to not get too bloated. Any alternative implementation ideas? |
@matthewd it seemed unavoidable but it was my first time digging into the adapter code so I could be wrong. I'm happy to make changes so let me know if you have any ideas. |
@matthewd I took another look at it. The latest commit moves most of the implementation into postgres-specific classes. I left some code in the |
03fc5b4
to
53cbda5
Compare
Rebased on master and squashed to 1 commit. @matthewd please advise if you want to see any more changes in this PR, otherwise I think it should be good to go. |
ccdff49
to
171ba30
Compare
171ba30
to
62a1548
Compare
62a1548
to
c893c26
Compare
Add validate_constraint and update naming
c893c26
to
8203482
Compare
Sorry I didn't come back to this sooner, especially after you did a great job of addressing my concern about the split between abstract vs postgres adapters. 👍🏻 |
# | ||
# Validates the constraint named +constraint_name+ on +accounts+. | ||
# | ||
# validate_foreign_key :accounts, :constraint_name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment doesn't match the method it's documenting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was fixed by 70c96b4
Summary
In Postgres, adding foreign keys can cause significant downtime because the transaction needs to acquire some very heavy locks on the table being altered as well as the table being referenced. To illustrate, if I wanted to add a foreign key on my
addresses
table referencing myusers
table:...my transaction acquires an
AccessExclusiveLock
onusers
which is extremely detrimental on a high-traffic table, esp. when Postgres performs a potentially lengthy query to validate the check.On the other hand, I can take a two-step approach which significantly reduces this burden; by introducing an invalid constraint in one transaction and validating it in another, the locks acquired are much less restrictive:
The first transaction acquires the same
AccessExclusiveLock
on theusers
table, but "the potentially-lengthy initial check to verify that all rows in the table satisfy the constraint is skipped" (source: postgres docs). Subsequently, the validation step does not block reads or writes on theusers
table. 💯So, this PR introduces two things:
valid: false
validate_foreign_key
method (which takes the same variety of params as the other foreign key methods) to validate a foreign keyI've heard rumors about this being on the roadmap for the Postgres team, i.e. skipping the check if the table being altered is empty and marking the constraint valid. In any case, perhaps someday this will be more easily achieved with built-in Postgres, but for now it's an issue.