-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Bring back heuristic fragment matching, with a twist. #6901
Merged
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
It's more convenient to configure possibleTypes as a map from supertypes to arrays of subtypes, since that's how a schema introspection query reports them. However, we can perform policies.fragmentMatches checks much more efficiently if we invert that structure internally, using a map from subtypes to sets of possible supertypes. When a fragment with type condition S is tested against an object with __typename T, we now search upwards from T through its supertypes until we find S (fragment matches), or the search terminates (matching fails). We can (and did) achieve the same results by starting from S and searching downward for T, but the branching factors tend to be larger in that direction, so the search tends to take longer.
A full explanation of these changes can be found in PR #6901.
a73517a
to
ae4a6f5
Compare
hwillson
approved these changes
Sep 10, 2020
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.
Incredible @benjamn - this looks awesome!
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Easily the biggest breaking change for fragment matching in Apollo Client 3.0 was the removal of the
FragmentMatcher
abstraction (#5073), along with its subclassesHeuristicFragmentMatcher
andIntrospectionFragmentmatcher
(#5684), which were replaced by the declarativepossibleTypes
configuration.The
HeuristicFragmentMatcher
was so named because it attempted to perform fragment matching without any actual knowledge of supertype/subtype relationships in your schema (for example, abstract interfaces implemented by concrete object subtypes, or unions with multiple alternative member types), checking instead whether all the fields of a given fragment were present in the result object, which is often a relatively strong signal that the fragment probably matched.The
HeuristicFragmentMatcher
could be fooled by field aliases and accidental sharing of field names between different fragments, but it was also relatively resilient to adding new subtypes on the server, because heuristic matching doesn't care what the true subtypes of a supertype are, so what's one more?Another big drawback of the
HeuristicFragmentMatcher
was that it applied the same fuzzy logic to reading from the cache, where the heuristic makes a lot less sense. If an individual query result that you're writing into the cache has all the keys you'd expect if a certain fragment matched, that's a pretty good sign that the fragment matched. But when you have a lot of data in your cache, from lots of different queries, it's not as meaningful to observe that all the fields required by some fragment you're reading are present in an object, since they might be there just because they've been written into the cache by other queries over time, not because the fragment actually matches the__typename
of the object.In moving to the more exact
possibleTypes
system, we gave up both the benefits and the drawbacks of theHeuristicFragmentMatcher
, replacing it with something functionally similar to theIntrospectionFragmentmatcher
, but with a much simpler configuration API.As #5750 demonstrates, the exactness of the
possibleTypes
system makes it challenging for existing clients to adapt when a new subtype is added on the server. Is there any way to make this system more flexible, like theHeuristicFragmentMatcher
, but without the drawbacks? I believe so!This PR improves the
possibleTypes
API by allowing "fuzzy" subtype strings, which are interpreted as regular expressions for type names, rather than as actual__typename
strings. If a fuzzy subtype matches the__typename
of an object, fragments on supertypes of that fuzzy subtype are allowed to match the object—provided the result also has all the keysrequired by the fragment.
In other words, if you previously configured the following
possibleTypes
:and you wanted to relax the
Test
supertype slightly, you could opt into fuzzy/heuristic matching by using a regular expression, while still enforcing a certain suffix:Since the
".*Test"
string is not a valid type name, it is interpreted as the regular expressionnew RegExp(".*Test")
. In order to count as a match, the regular expression must match the entire__typename
string, and the match will only be honored if all the fragment's fields are actually present in the result object. BecausePassingTest
andFailingTest
are still specified explicitly, they will continue to match immediately, without any fuzzy logic, but an object with__typename
equal toSkippedTest
orWishfulTest
would now have a chance of qualifying as aTest
for the purposes of fragment matching.Likewise, if you added
Python: ["^[A-Z].*Python"]
to thepossibleTypes
list above, afragment SnakeFragment on Snake
could match an object with a__typename
ofReticulatedPython
, provided that object has all the necessary fragment fields.The key advantages of this new system compared to the
HeuristicFragmentMatcher
are that (1) you can specify fuzzy subtypes for specific supertypes (rather than applying heuristic matching for all types by default), and (2) that it "learns" about fuzzy subtypes while writing (where the heuristic tends to work well), and then merely uses those inferred supertype/subtype relationships when reading, so there is no need to perform heuristic matching while reading. This is the "twist" mentioned in the title of this PR. When one of these inferences happens, you'll see a warning in the console (in development).Unlike the old
HeuristicFragmentMatcher
, which provided the default behavior for all fragment matching, you do have to opt into fuzzy matching withpossibleTypes
, but it can be a useful strategy for preparing your clients for upcoming server changes, or for relaxing the rules for a particular supertype, in cases when you anticipate the subtypes may soon change on the server.