Skip to content
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

Make the Relation -> Model delegation stricter #50396

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

casperisfine
Copy link
Contributor

In #50395 I noticed lots of methods are delegated from Relation to the model. The intent of this code is to allow using use defined class methods like scopes.

But because of this autmated delegation it allowed calling any ActiveRecord::Base class method on a Relation, which in itself may be desireable, however we very wastefully define the delegator on the first call, and worse we wrap it with a current scope setter.

So I think we should be more strict about it.

NB: this of course breaks a tons of test, so I need to find more time to finish this, and it likely need a config flag and a deprecation notice of some sort. Or alternatively we can just accept this API, but delegate all the AR::Base class methods eagerly and without the scoping overhead. TBD.

@casperisfine casperisfine marked this pull request as draft December 19, 2023 10:56
@casperisfine casperisfine force-pushed the stricter-relation-delegation branch 2 times, most recently from 0fe6cde to 5c84152 Compare January 15, 2024 16:52
@casperisfine
Copy link
Contributor Author

Note to self:

  • It's hard to know for sure the list of methods in AR::Base that require this scoped delegation
  • We should make sure that if we get this wrong it end up in NoMethodError or in a scoped delegate, never in an unscoped-delegate, as it could lead to unscoped queries leaking or deleting data by accident (would be super bad).

:sanitize_sql_like, :unscoped, to: :klass

# TODO: scoped delegate
[:find_signed, :find_signed!, :delete, :find_by_token_for, :find_by_token_for!, :upsert_all, :insert_all, :insert_all!].each do |method|
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This list is very likely incomplete.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I rebased, and this is still the main blocker. We have a number of ActiveRecord::Base class methods that rely on this scoped delegation.

I need to find a good alternative.

@rafaelfranca
Copy link
Member

Makes sense

@jaynetics
Copy link
Contributor

perhaps relevant: #50760

@casperisfine
Copy link
Contributor Author

perhaps relevant: #50760

Yeah, it's in #50395, which in a way is similar, that I noticed this problem.

casperisfine pushed a commit to Shopify/rails that referenced this pull request May 10, 2024
Fix: rails#51775

This us us being bitten by rails#50396
once more. We should really make this delegation much stricter.
@casperisfine
Copy link
Contributor Author

Got bit by this again: #51776, I really need to finish this PR somehow.

@casperisfine casperisfine force-pushed the stricter-relation-delegation branch 4 times, most recently from 8127973 to fcde614 Compare May 10, 2024 08:40
@byroot byroot marked this pull request as ready for review May 10, 2024 08:51
casperisfine pushed a commit to Shopify/rails that referenced this pull request May 13, 2024
Ref: rails#50396
Ref: rails#51776

`ActiveRecord::Relation` automatically delegates missing methods
to the model class wrapped in a `scoping { }` block.

This is to support scoping in user defined class methods. The problem
however is that it's very error prone for the framework, because we
can mistakenly call model methods from inside `Relation` and not
realized we're applying a global scope.

In the best case scenario it's just a waste of performance, but
it can also lead to bugs like rails#51775

I'm planning to restrict this automatic delegation to methods defined
in childs of `ActiveRecord::Base` only: rails#50396
but for this to work we must first refactor any Rails code that rely on it.
casperisfine pushed a commit to Shopify/rails that referenced this pull request May 13, 2024
Ref: rails#50396
Ref: rails#51776

`ActiveRecord::Relation` automatically delegates missing methods
to the model class wrapped in a `scoping { }` block.

This is to support scoping in user defined class methods. The problem
however is that it's very error prone for the framework, because we
can mistakenly call model methods from inside `Relation` and not
realized we're applying a global scope.

In the best case scenario it's just a waste of performance, but
it can also lead to bugs like rails#51775

I'm planning to restrict this automatic delegation to methods defined
in childs of `ActiveRecord::Base` only: rails#50396
but for this to work we must first refactor any Rails code that rely on it.
casperisfine pushed a commit to Shopify/rails that referenced this pull request May 13, 2024
Ref: rails#50396
Ref: rails#51776

`ActiveRecord::Relation` automatically delegates missing methods
to the model class wrapped in a `scoping { }` block.

This is to support scoping in user defined class methods. The problem
however is that it's very error prone for the framework, because we
can mistakenly call model methods from inside `Relation` and not
realized we're applying a global scope.

In the best case scenario it's just a waste of performance, but
it can also lead to bugs like rails#51775

I'm planning to restrict this automatic delegation to methods defined
in childs of `ActiveRecord::Base` only: rails#50396
but for this to work we must first refactor any Rails code that rely on it.
casperisfine pushed a commit to Shopify/rails that referenced this pull request May 13, 2024
Ref: rails#50396
Ref: rails#51776

`ActiveRecord::Relation` automatically delegates missing methods
to the model class wrapped in a `scoping { }` block.

This is to support scoping in user defined class methods. The problem
however is that it's very error prone for the framework, because we
can mistakenly call model methods from inside `Relation` and not
realized we're applying a global scope.

In the best case scenario it's just a waste of performance, but
it can also lead to bugs like rails#51775

I'm planning to restrict this automatic delegation to methods defined
in childs of `ActiveRecord::Base` only: rails#50396
but for this to work we must first refactor any Rails code that rely on it.
casperisfine pushed a commit to Shopify/rails that referenced this pull request May 13, 2024
Ref: rails#50396
Ref: rails#51776

`ActiveRecord::Relation` automatically delegates missing methods
to the model class wrapped in a `scoping { }` block.

This is to support scoping in user defined class methods. The problem
however is that it's very error prone for the framework, because we
can mistakenly call model methods from inside `Relation` and not
realized we're applying a global scope.

In the best case scenario it's just a waste of performance, but
it can also lead to bugs like rails#51775

I'm planning to restrict this automatic delegation to methods defined
in childs of `ActiveRecord::Base` only: rails#50396
but for this to work we must first refactor any Rails code that rely on it.
casperisfine pushed a commit to Shopify/rails that referenced this pull request May 13, 2024
casperisfine pushed a commit to Shopify/rails that referenced this pull request May 13, 2024
casperisfine pushed a commit to Shopify/rails that referenced this pull request May 13, 2024
@casperisfine casperisfine force-pushed the stricter-relation-delegation branch 3 times, most recently from 53b00de to c28d7a9 Compare May 13, 2024 07:40
In rails#50395 I noticed lots of
methods are delegated from `Relation` to the model. The intent of
this code is to allow using use defined class methods like scopes.

But because of this autmated delegation it allowed calling any
`ActiveRecord::Base` class method on a `Relation`, which in itself
may be desireable, however we very wastefully define the delegator
on the first call, and worse we wrap it with a current scope setter.

So I think we should be more strict about it.
@@ -100,7 +100,7 @@ def #{method}(...)
:to_sentence, :to_fs, :to_formatted_s, :as_json,
:shuffle, :split, :slice, :index, :rindex, to: :records

delegate :primary_key, :lease_connection, :connection, :with_connection, :transaction, to: :klass
delegate :primary_key, :with_connection, :connection, :table_name, :transaction, :sanitize_sql_like, :unscoped, to: :klass
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite sure whether we should keep any of these.

fractaledmind pushed a commit to fractaledmind/rails that referenced this pull request May 13, 2024
Fix: rails#51775

This us us being bitten by rails#50396
once more. We should really make this delegation much stricter.
fractaledmind pushed a commit to fractaledmind/rails that referenced this pull request May 13, 2024
Ref: rails#50396
Ref: rails#51776

`ActiveRecord::Relation` automatically delegates missing methods
to the model class wrapped in a `scoping { }` block.

This is to support scoping in user defined class methods. The problem
however is that it's very error prone for the framework, because we
can mistakenly call model methods from inside `Relation` and not
realized we're applying a global scope.

In the best case scenario it's just a waste of performance, but
it can also lead to bugs like rails#51775

I'm planning to restrict this automatic delegation to methods defined
in childs of `ActiveRecord::Base` only: rails#50396
but for this to work we must first refactor any Rails code that rely on it.
fractaledmind pushed a commit to fractaledmind/rails that referenced this pull request May 13, 2024
Ref: rails#50396
Ref: rails#51776

`ActiveRecord::Relation` automatically delegates missing methods
to the model class wrapped in a `scoping { }` block.

This is to support scoping in user defined class methods. The problem
however is that it's very error prone for the framework, because we
can mistakenly call model methods from inside `Relation` and not
realized we're applying a global scope.

In the best case scenario it's just a waste of performance, but
it can also lead to bugs like rails#51775

I'm planning to restrict this automatic delegation to methods defined
in childs of `ActiveRecord::Base` only: rails#50396
but for this to work we must first refactor any Rails code that rely on it.
fractaledmind pushed a commit to fractaledmind/rails that referenced this pull request May 13, 2024
fractaledmind pushed a commit to fractaledmind/rails that referenced this pull request May 13, 2024
fractaledmind pushed a commit to fractaledmind/rails that referenced this pull request May 13, 2024
@casperisfine
Copy link
Contributor Author

I slept on this one. I think long term we should deprecate most of these delegations, but short term we can simply make it so that we still delegate for methods present in ActiveRecord::Base, but without the scoping around it.

I also want to ship the call site cleanup part.

dhh pushed a commit that referenced this pull request May 14, 2024
Ref: #50396
Ref: #51776

`ActiveRecord::Relation` automatically delegates missing methods
to the model class wrapped in a `scoping { }` block.

This is to support scoping in user defined class methods. The problem
however is that it's very error prone for the framework, because we
can mistakenly call model methods from inside `Relation` and not
realized we're applying a global scope.

In the best case scenario it's just a waste of performance, but
it can also lead to bugs like #51775

I'm planning to restrict this automatic delegation to methods defined
in childs of `ActiveRecord::Base` only: #50396
but for this to work we must first refactor any Rails code that rely on it.
dhh pushed a commit that referenced this pull request May 14, 2024
Ref: #50396
Ref: #51776

`ActiveRecord::Relation` automatically delegates missing methods
to the model class wrapped in a `scoping { }` block.

This is to support scoping in user defined class methods. The problem
however is that it's very error prone for the framework, because we
can mistakenly call model methods from inside `Relation` and not
realized we're applying a global scope.

In the best case scenario it's just a waste of performance, but
it can also lead to bugs like #51775

I'm planning to restrict this automatic delegation to methods defined
in childs of `ActiveRecord::Base` only: #50396
but for this to work we must first refactor any Rails code that rely on it.
dhh pushed a commit that referenced this pull request May 14, 2024
dhh pushed a commit that referenced this pull request May 14, 2024
dhh pushed a commit that referenced this pull request May 14, 2024
Ref: #50396

As well as related methods.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants