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

Avoid modifying query graph edge conditions for @requires #1783

Merged
merged 4 commits into from Apr 29, 2022

Conversation

pcmanus
Copy link
Contributor

@pcmanus pcmanus commented Apr 26, 2022

The problem this fixes is that SelectionSet is mutable, but query
graphs needs to be immutable post-construction since they are reused
between query plans, and the edge of those use SelectionSet for the
conditions. As a result, it was a bit too easy to inadvertently
modify an edge conditions without meaning to, which is exactly what
was happens in the handling of requires.

Note that we could manually fix the exact issue in the requires-handling
code by manually cloning the edge conditions we weren't properly
cloning, but this commit take a slightly more systemic approach
that attempts to make it harder for such issue to popup in another
place later.

That is, this adds the possibility for "freezing" a SelectionSet,
which makes it effectively immutable from that point on. This should
allow to find unexpected modification to edge conditions much sooner
during testing. But this also allow to automatically clone frozen
selection sets when they would otherwise be embeded/aliased in other
non-frozen (and thus mutable) sets, which is actually the issue
from #1750.

Fixes #1750

@netlify
Copy link

netlify bot commented Apr 26, 2022

👷 Deploy request for apollo-federation-docs pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 790a877

@codesandbox-ci
Copy link

codesandbox-ci bot commented Apr 26, 2022

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@@ -542,6 +542,7 @@ export class SelectionSet {
private readonly _selections = new MultiMap<string, Selection>();
Copy link
Contributor

Choose a reason for hiding this comment

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

I was thinking about potentially doing Freezable as a mixin class so that you could have only one implementation, but I think that may get a little confusing. Maybe add an interface instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added an abstract class to save duplication when possible.

expect(() => s.mergeIn(s2)).toThrowError('Cannot add to frozen selection: { t { a } }');
});

it('it does not clone non-frozen selections when adding to another one', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should you have a test that checks that updateForAddingTo will clone if necessary or is that covered by one of these cases?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The test using mergeIn indirectly test it, as it's the cloning in this method that end up ensuring SelectionSet::add and SelectionSet::mergeIn do a clone. In fact, updateForAddingTo is not really meant to be use externally, it's only public because there is no simple way to define a method in Selection that only SelectionSet can use (well, I can I can make it private and cheat the type-system with prototype, but didn't wanted).

That said, I'll admit I didn't overdo testing here, and I could probably add at least a few test to exercise fragment selections, and why not direct test for both add and updateForAddingTo.

add(selection: Selection): Selection {
// It's a but to try to add to a frozen selection set
assert(!this.isFrozen(), () => `Cannot add to frozen selection: ${this}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't need to add this assertion to other methods? i.e addPath, etc?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The code make sure all modifications are done by this method, so that one should be sufficient in practice.

// edge freely). Which means we need to clone any existing conditions (so we can modify them), and need
// to re-freeze the result afterwards.
this._conditions = this._conditions
? this._conditions.clone()
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this clone() necessary? Aren't we already cloning in the constructor?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We're also freezing in the constructor, and here we're trying to mutate the conditions to add new ones. So that clone is essentially about "unfreezing" the selections (but in theory, we could add an unfreeze method that just flip back the _isFrozen boolean, and that would be enough for that one specific case; but an unfreeze method makes the frozen story a bit dodgy and I don't think cloning here matters performance wise so ...).

@pcmanus
Copy link
Contributor Author

pcmanus commented Apr 29, 2022

Took the liberty to rebase because there was some conflict in buildPlan.test.ts given recent commits to main, but kept the review-changes separate. Along with the new Freezable abstraction, I added a few additional unit tests. I'll admit that I haven't covered all possible permutations that could be tested, but knowing how the code is organised, I felt that I was starting to hit diminishing returns (and I'm eager to get this fix released).

@pcmanus pcmanus requested a review from clenfest April 29, 2022 10:10
Sylvain Lebresne added 4 commits April 29, 2022 17:35
The problem this fixes is that `SelectionSet` is mutable, but query
graphs needs to be immutable post-construction since they are reused
between query plans, and the edge of those use `SelectionSet` for the
conditions. As a result, it was a bit too easy to inadvertently
modify an edge conditions without meaning to, which is exactly what
was happens in the handling of requires.

Note that we could manually fix the exact issue in the requires-handling
code by manually cloning the edge conditions we weren't properly
cloning, but this commit take a slightly more systemic approach
that attempts to make it harder for such issue to popup in another
place later.

That is, this adds the possibility for "freezing" a `SelectionSet`,
which makes it effectively immutable from that point on. This should
allow to find unexpected modification to edge conditions much sooner
during testing. But this also allow to automatically clone frozen
selection sets when they would otherwise be embeded/aliased in other
non-frozen (and thus mutable) sets, which is actually the issue
from apollographql#1750.

Fixes apollographql#1750
@pcmanus pcmanus merged commit a0c7343 into apollographql:main Apr 29, 2022
This was referenced Apr 29, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Plans of one query incorrectly alters the plans of other queries when there are requires with nested fields
2 participants