From 065d3175ad7e16af6ce21f6cbe16750cc945356b Mon Sep 17 00:00:00 2001
From: Ben Newman <ben@apollographql.com>
Date: Sat, 22 Aug 2020 15:30:22 -0400
Subject: [PATCH 1/7] Use getFragmentFromSelection helper in
 executeSelectionSet.

---
 src/cache/inmemory/readFromStore.ts | 23 +++++++----------------
 1 file changed, 7 insertions(+), 16 deletions(-)

diff --git a/src/cache/inmemory/readFromStore.ts b/src/cache/inmemory/readFromStore.ts
index 85824c1ef28..1abafb40f30 100644
--- a/src/cache/inmemory/readFromStore.ts
+++ b/src/cache/inmemory/readFromStore.ts
@@ -1,8 +1,6 @@
 import {
   DocumentNode,
   FieldNode,
-  FragmentDefinitionNode,
-  InlineFragmentNode,
   SelectionSetNode,
 } from 'graphql';
 import { wrap, OptimisticWrapperFunction } from 'optimism';
@@ -10,7 +8,6 @@ import { invariant, InvariantError } from 'ts-invariant';
 
 import {
   isField,
-  isInlineFragment,
   resultKeyNameFromField,
   Reference,
   isReference,
@@ -26,6 +23,7 @@ import {
   getQueryDefinition,
   maybeDeepFreeze,
   mergeDeepArray,
+  getFragmentFromSelection,
 } from '../../utilities';
 import { Cache } from '../core/types/Cache';
 import {
@@ -201,7 +199,7 @@ export class StoreReader {
       };
     }
 
-    const { fragmentMap, variables, policies, store } = context;
+    const { variables, policies, store } = context;
     const objectsToMerge: { [key: string]: any }[] = [];
     const finalResult: ExecResult = { result: null };
     const typename = store.getFieldValue<string>(objectOrReference, "__typename");
@@ -313,19 +311,12 @@ export class StoreReader {
         invariant(context.path.pop() === resultName);
 
       } else {
-        let fragment: InlineFragmentNode | FragmentDefinitionNode;
-
-        if (isInlineFragment(selection)) {
-          fragment = selection;
-        } else {
-          // This is a named fragment
-          invariant(
-            fragment = fragmentMap[selection.name.value],
-            `No fragment named ${selection.name.value}`,
-          );
-        }
+        const fragment = getFragmentFromSelection(
+          selection,
+          context.fragmentMap,
+        );
 
-        if (policies.fragmentMatches(fragment, typename)) {
+        if (fragment && policies.fragmentMatches(fragment, typename)) {
           fragment.selectionSet.selections.forEach(workSet.add, workSet);
         }
       }

From 990a488ec0242131fd4957eadfd5f95b7c29898a Mon Sep 17 00:00:00 2001
From: Ben Newman <ben@apollographql.com>
Date: Fri, 21 Aug 2020 14:33:07 -0400
Subject: [PATCH 2/7] Invert storage of subtype-supertype relationships in
 Policies class.

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.
---
 src/cache/inmemory/policies.ts | 65 +++++++++++++++++++++-------------
 1 file changed, 41 insertions(+), 24 deletions(-)

diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts
index c32ede8c92a..4ddf39da6a1 100644
--- a/src/cache/inmemory/policies.ts
+++ b/src/cache/inmemory/policies.ts
@@ -227,7 +227,6 @@ export class Policies {
   private typePolicies: {
     [__typename: string]: {
       keyFn?: KeyFieldsFunction;
-      subtypes?: Set<string>;
       fields?: {
         [fieldName: string]: {
           keyFn?: KeyArgsFunction;
@@ -238,6 +237,12 @@ export class Policies {
     };
   } = Object.create(null);
 
+  // Map from subtype names to sets of supertype names. Note that this
+  // representation inverts the structure of possibleTypes (whose keys are
+  // supertypes and whose values are arrays of subtypes) because it tends
+  // to be much more efficient to search upwards than downwards.
+  private supertypeMap = new Map<string, Set<string>>();
+
   public readonly cache: InMemoryCache;
 
   public readonly rootIdsByTypename: Record<string, string> = Object.create(null);
@@ -407,8 +412,9 @@ export class Policies {
   public addPossibleTypes(possibleTypes: PossibleTypesMap) {
     (this.usingPossibleTypes as boolean) = true;
     Object.keys(possibleTypes).forEach(supertype => {
-      const subtypeSet = this.getSubtypeSet(supertype, true);
-      possibleTypes[supertype].forEach(subtypeSet!.add, subtypeSet);
+      possibleTypes[supertype].forEach(subtype => {
+        this.getSupertypeSet(subtype, true)!.add(supertype);
+      });
     });
   }
 
@@ -422,17 +428,6 @@ export class Policies {
     }
   }
 
-  private getSubtypeSet(
-    supertype: string,
-    createIfMissing: boolean,
-  ): Set<string> | undefined {
-    const policy = this.getTypePolicy(supertype, createIfMissing);
-    if (policy) {
-      return policy.subtypes || (
-        createIfMissing ? policy.subtypes = new Set<string>() : void 0);
-    }
-  }
-
   private getFieldPolicy(
     typename: string | undefined,
     fieldName: string,
@@ -453,6 +448,17 @@ export class Policies {
     }
   }
 
+  private getSupertypeSet(
+    subtype: string,
+    createIfMissing: boolean,
+  ): Set<string> | undefined {
+    let supertypeSet = this.supertypeMap.get(subtype);
+    if (!supertypeSet && createIfMissing) {
+      this.supertypeMap.set(subtype, supertypeSet = new Set<string>());
+    }
+    return supertypeSet;
+  }
+
   public fragmentMatches(
     fragment: InlineFragmentNode | FragmentDefinitionNode,
     typename: string | undefined,
@@ -464,23 +470,34 @@ export class Policies {
     if (!typename) return false;
 
     const supertype = fragment.typeCondition.name.value;
+    // Common case: fragment type condition and __typename are the same.
     if (typename === supertype) return true;
 
     if (this.usingPossibleTypes) {
-      const workQueue = [this.getSubtypeSet(supertype, false)];
+      const typenameSupertypeSet = this.getSupertypeSet(typename, true)!;
+      const workQueue = [typenameSupertypeSet];
+      const maybeEnqueue = (subtype: string) => {
+        const supertypeSet = this.getSupertypeSet(subtype, false);
+        if (supertypeSet && workQueue.indexOf(supertypeSet) < 0) {
+          workQueue.push(supertypeSet);
+        }
+      };
+
       // It's important to keep evaluating workQueue.length each time through
       // the loop, because the queue can grow while we're iterating over it.
       for (let i = 0; i < workQueue.length; ++i) {
-        const subtypes = workQueue[i];
-        if (subtypes) {
-          if (subtypes.has(typename)) return true;
-          subtypes.forEach(subtype => {
-            const subsubtypes = this.getSubtypeSet(subtype, false);
-            if (subsubtypes && workQueue.indexOf(subsubtypes) < 0) {
-              workQueue.push(subsubtypes);
-            }
-          });
+        const supertypeSet = workQueue[i];
+
+        if (supertypeSet.has(supertype)) {
+          // Record positive results for faster future lookup.
+          // Unfortunately, we cannot safely cache negative results,
+          // because new possibleTypes data could always be added to the
+          // Policies class.
+          typenameSupertypeSet.add(supertype);
+          return true;
         }
+
+        supertypeSet.forEach(maybeEnqueue);
       }
     }
 

From 1c54969c831fe6a437c8d8ded2b641723bc27ed8 Mon Sep 17 00:00:00 2001
From: Ben Newman <ben@apollographql.com>
Date: Sat, 22 Aug 2020 14:23:58 -0400
Subject: [PATCH 3/7] Bring back heuristic fragment matching, with a twist.

A full explanation of these changes can be found in PR #6901.
---
 src/cache/inmemory/helpers.ts      | 35 +++++++++++++--
 src/cache/inmemory/policies.ts     | 68 ++++++++++++++++++++++++++++++
 src/cache/inmemory/writeToStore.ts | 21 ++++++++-
 3 files changed, 120 insertions(+), 4 deletions(-)

diff --git a/src/cache/inmemory/helpers.ts b/src/cache/inmemory/helpers.ts
index 9cdd09ae12c..47d9ee28e5c 100644
--- a/src/cache/inmemory/helpers.ts
+++ b/src/cache/inmemory/helpers.ts
@@ -1,4 +1,4 @@
-import { FieldNode } from 'graphql';
+import { FieldNode, SelectionSetNode } from 'graphql';
 
 import { NormalizedCache } from './types';
 import {
@@ -9,6 +9,8 @@ import {
   isField,
   DeepMerger,
   ReconcilerFunction,
+  resultKeyNameFromField,
+  shouldInclude,
 } from '../../utilities';
 
 export const hasOwn = Object.prototype.hasOwnProperty;
@@ -22,12 +24,39 @@ export function getTypenameFromStoreObject(
     : objectOrReference && objectOrReference.__typename;
 }
 
-const FieldNamePattern = /^[_A-Za-z0-9]+/;
+export const TypeOrFieldNameRegExp = /^[_a-z][_0-9a-z]*/i;
+
 export function fieldNameFromStoreName(storeFieldName: string): string {
-  const match = storeFieldName.match(FieldNamePattern);
+  const match = storeFieldName.match(TypeOrFieldNameRegExp);
   return match ? match[0] : storeFieldName;
 }
 
+export function selectionSetMatchesResult(
+  selectionSet: SelectionSetNode,
+  result: Record<string, any>,
+  variables?: Record<string, any>,
+): boolean {
+  if (result && typeof result === "object") {
+    return Array.isArray(result)
+      ? result.every(item => selectionSetMatchesResult(selectionSet, item, variables))
+      : selectionSet.selections.every(field => {
+        if (isField(field) && shouldInclude(field, variables)) {
+          const key = resultKeyNameFromField(field);
+          return hasOwn.call(result, key) &&
+            (!field.selectionSet ||
+             selectionSetMatchesResult(field.selectionSet, result[key], variables));
+        }
+        // If the selection has been skipped with @skip(true) or
+        // @include(false), it should not count against the matching. If
+        // the selection is not a field, it must be a fragment (inline or
+        // named). We will determine if selectionSetMatchesResult for that
+        // fragment when we get to it, so for now we return true.
+        return true;
+      });
+  }
+  return false;
+}
+
 // Invoking merge functions needs to happen after processSelectionSet has
 // finished, but requires information that is more readily available
 // during processSelectionSet, so processSelectionSet embeds special
diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts
index 4ddf39da6a1..133ddb5246e 100644
--- a/src/cache/inmemory/policies.ts
+++ b/src/cache/inmemory/policies.ts
@@ -29,6 +29,8 @@ import {
   FieldValueToBeMerged,
   isFieldValueToBeMerged,
   storeValueIsStoreObject,
+  selectionSetMatchesResult,
+  TypeOrFieldNameRegExp,
 } from './helpers';
 import { cacheSlot } from './reactiveVars';
 import { InMemoryCache } from './inMemoryCache';
@@ -243,6 +245,12 @@ export class Policies {
   // to be much more efficient to search upwards than downwards.
   private supertypeMap = new Map<string, Set<string>>();
 
+  // Any fuzzy subtypes specified by possibleTypes will be converted to
+  // RegExp objects and recorded here. Every key of this map can also be
+  // found in supertypeMap. In many cases this Map will be empty, which
+  // means no fuzzy subtype checking will happen in fragmentMatches.
+  private fuzzySubtypes = new Map<string, RegExp>();
+
   public readonly cache: InMemoryCache;
 
   public readonly rootIdsByTypename: Record<string, string> = Object.create(null);
@@ -414,6 +422,11 @@ export class Policies {
     Object.keys(possibleTypes).forEach(supertype => {
       possibleTypes[supertype].forEach(subtype => {
         this.getSupertypeSet(subtype, true)!.add(supertype);
+        const match = subtype.match(TypeOrFieldNameRegExp);
+        if (!match || match[0] !== subtype) {
+          // TODO Don't interpret just any invalid typename as a RegExp.
+          this.fuzzySubtypes.set(subtype, new RegExp(subtype));
+        }
       });
     });
   }
@@ -462,6 +475,8 @@ export class Policies {
   public fragmentMatches(
     fragment: InlineFragmentNode | FragmentDefinitionNode,
     typename: string | undefined,
+    result?: Record<string, any>,
+    variables?: Record<string, any>,
   ): boolean {
     if (!fragment.typeCondition) return true;
 
@@ -483,12 +498,40 @@ export class Policies {
         }
       };
 
+      // We need to check fuzzy subtypes only if we encountered fuzzy
+      // subtype strings in addPossibleTypes, and only while writing to
+      // the cache, since that's when selectionSetMatchesResult gives a
+      // strong signal of fragment matching. The StoreReader class calls
+      // policies.fragmentMatches without passing a result object, so
+      // needToCheckFuzzySubtypes is always false while reading.
+      let needToCheckFuzzySubtypes = !!(result && this.fuzzySubtypes.size);
+
+      // We don't need to check selectionSetMatchesResult for known
+      // supertype/subtype relationships specified in possibleTypes, but
+      // we will need to enforce that verification once we begin checking
+      // fuzzy subtypes.
+      let mustVerifyResult = false;
+
       // It's important to keep evaluating workQueue.length each time through
       // the loop, because the queue can grow while we're iterating over it.
       for (let i = 0; i < workQueue.length; ++i) {
         const supertypeSet = workQueue[i];
 
         if (supertypeSet.has(supertype)) {
+          if (result && mustVerifyResult) {
+            // Since this verification doesn't depend on any for-loop
+            // variables, we could technically hoist it outside the loop,
+            // but the verification can be expensive, so we postpone it
+            // until the last possible moment (here).
+            if (selectionSetMatchesResult(fragment.selectionSet, result, variables)) {
+              invariant.warn(`Inferring subtype ${typename} of supertype ${supertype}`);
+            } else {
+              // Since the result of the verification isn't going to
+              // change the next time we do it, we can go ahead and
+              // terminate the search at this point.
+              return false;
+            }
+          }
           // Record positive results for faster future lookup.
           // Unfortunately, we cannot safely cache negative results,
           // because new possibleTypes data could always be added to the
@@ -498,6 +541,31 @@ export class Policies {
         }
 
         supertypeSet.forEach(maybeEnqueue);
+
+        // We start checking fuzzy subtypes only after we've exhausted all
+        // non-fuzzy subtypes.
+        const isFinalIteration = i === workQueue.length - 1;
+        if (isFinalIteration && needToCheckFuzzySubtypes) {
+          // Check fuzzy subtypes at most once (that is, don't run the
+          // this.fuzzySubtypes.forEach loop more than once).
+          needToCheckFuzzySubtypes = false;
+
+          // Now that we're checking fuzzy subtypes, enforce heuristic
+          // fragment/result matching before returning true if/when we
+          // find a matching supertype.
+          mustVerifyResult = true;
+
+          // If we find any fuzzy subtypes that match typename, extend the
+          // workQueue to search through the supertypes of those fuzzy
+          // subtypes. Otherwise the for-loop will terminate and we'll
+          // return false below.
+          this.fuzzySubtypes.forEach((regExp, fuzzyString) => {
+            const match = typename.match(regExp);
+            if (match && match[0] === typename) {
+              maybeEnqueue(fuzzyString);
+            }
+          });
+        }
       }
     }
 
diff --git a/src/cache/inmemory/writeToStore.ts b/src/cache/inmemory/writeToStore.ts
index 25feb11be28..1d05ba8e3f2 100644
--- a/src/cache/inmemory/writeToStore.ts
+++ b/src/cache/inmemory/writeToStore.ts
@@ -245,7 +245,26 @@ export class StoreWriter {
           context.fragmentMap,
         );
 
-        if (fragment && policies.fragmentMatches(fragment, typename)) {
+        if (fragment &&
+            // By passing result and context.variables, we enable
+            // policies.fragmentMatches to bend the rules when typename is
+            // not a known subtype of the fragment type condition, but the
+            // result object contains all the keys requested by the
+            // fragment, which strongly suggests the fragment probably
+            // matched. This fuzzy matching behavior must be enabled by
+            // including a regular expression string (such as ".*" or
+            // "Prefix.*" or ".*Suffix") in the possibleTypes array for
+            // specific supertypes; otherwise, all matching remains exact.
+            // Fuzzy matches are remembered by the Policies object and
+            // later used when reading from the cache. Since there is no
+            // incoming result object to check when reading, reading does
+            // not involve the same fuzzy inference, so the StoreReader
+            // class calls policies.fragmentMatches without passing result
+            // or context.variables. The flexibility of fuzzy matching
+            // allows existing clients to accommodate previously unknown
+            // __typename strings produced by server/schema changes, which
+            // would otherwise be breaking changes.
+            policies.fragmentMatches(fragment, typename, result, context.variables)) {
           fragment.selectionSet.selections.forEach(workSet.add, workSet);
         }
       }

From 92a04db4e7d06c05ecfe326c4443386a1a1bf1dd Mon Sep 17 00:00:00 2001
From: Ben Newman <ben@apollographql.com>
Date: Sat, 22 Aug 2020 14:53:25 -0400
Subject: [PATCH 4/7] Return early when inexact fragment type condition is
 unknown.

---
 src/cache/inmemory/policies.ts | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts
index 133ddb5246e..f2018e53e4f 100644
--- a/src/cache/inmemory/policies.ts
+++ b/src/cache/inmemory/policies.ts
@@ -420,6 +420,11 @@ export class Policies {
   public addPossibleTypes(possibleTypes: PossibleTypesMap) {
     (this.usingPossibleTypes as boolean) = true;
     Object.keys(possibleTypes).forEach(supertype => {
+      // Make sure all types have an entry in this.supertypeMap, even if
+      // their supertype set is empty, so we can return false immediately
+      // from policies.fragmentMatches for unknown supertypes.
+      this.getSupertypeSet(supertype, true);
+
       possibleTypes[supertype].forEach(subtype => {
         this.getSupertypeSet(subtype, true)!.add(supertype);
         const match = subtype.match(TypeOrFieldNameRegExp);
@@ -488,12 +493,15 @@ export class Policies {
     // Common case: fragment type condition and __typename are the same.
     if (typename === supertype) return true;
 
-    if (this.usingPossibleTypes) {
+    if (this.usingPossibleTypes &&
+        this.supertypeMap.has(supertype)) {
       const typenameSupertypeSet = this.getSupertypeSet(typename, true)!;
       const workQueue = [typenameSupertypeSet];
       const maybeEnqueue = (subtype: string) => {
         const supertypeSet = this.getSupertypeSet(subtype, false);
-        if (supertypeSet && workQueue.indexOf(supertypeSet) < 0) {
+        if (supertypeSet &&
+            supertypeSet.size &&
+            workQueue.indexOf(supertypeSet) < 0) {
           workQueue.push(supertypeSet);
         }
       };

From fcbc86f45ccbf6f3dedd6d31beb5e682f4e896d7 Mon Sep 17 00:00:00 2001
From: Ben Newman <ben@apollographql.com>
Date: Wed, 26 Aug 2020 11:45:01 -0400
Subject: [PATCH 5/7] Check selectionSetMatchesResult before checking fuzzy
 subtypes.

---
 src/cache/inmemory/policies.ts | 54 +++++++++++++---------------------
 1 file changed, 21 insertions(+), 33 deletions(-)

diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts
index f2018e53e4f..d36974b0845 100644
--- a/src/cache/inmemory/policies.ts
+++ b/src/cache/inmemory/policies.ts
@@ -513,12 +513,7 @@ export class Policies {
       // policies.fragmentMatches without passing a result object, so
       // needToCheckFuzzySubtypes is always false while reading.
       let needToCheckFuzzySubtypes = !!(result && this.fuzzySubtypes.size);
-
-      // We don't need to check selectionSetMatchesResult for known
-      // supertype/subtype relationships specified in possibleTypes, but
-      // we will need to enforce that verification once we begin checking
-      // fuzzy subtypes.
-      let mustVerifyResult = false;
+      let checkingFuzzySubtypes = false;
 
       // It's important to keep evaluating workQueue.length each time through
       // the loop, because the queue can grow while we're iterating over it.
@@ -526,42 +521,35 @@ export class Policies {
         const supertypeSet = workQueue[i];
 
         if (supertypeSet.has(supertype)) {
-          if (result && mustVerifyResult) {
-            // Since this verification doesn't depend on any for-loop
-            // variables, we could technically hoist it outside the loop,
-            // but the verification can be expensive, so we postpone it
-            // until the last possible moment (here).
-            if (selectionSetMatchesResult(fragment.selectionSet, result, variables)) {
+          if (!typenameSupertypeSet.has(supertype)) {
+            if (checkingFuzzySubtypes) {
               invariant.warn(`Inferring subtype ${typename} of supertype ${supertype}`);
-            } else {
-              // Since the result of the verification isn't going to
-              // change the next time we do it, we can go ahead and
-              // terminate the search at this point.
-              return false;
             }
+            // Record positive results for faster future lookup.
+            // Unfortunately, we cannot safely cache negative results,
+            // because new possibleTypes data could always be added to the
+            // Policies class.
+            typenameSupertypeSet.add(supertype);
           }
-          // Record positive results for faster future lookup.
-          // Unfortunately, we cannot safely cache negative results,
-          // because new possibleTypes data could always be added to the
-          // Policies class.
-          typenameSupertypeSet.add(supertype);
           return true;
         }
 
         supertypeSet.forEach(maybeEnqueue);
 
-        // We start checking fuzzy subtypes only after we've exhausted all
-        // non-fuzzy subtypes.
-        const isFinalIteration = i === workQueue.length - 1;
-        if (isFinalIteration && needToCheckFuzzySubtypes) {
-          // Check fuzzy subtypes at most once (that is, don't run the
-          // this.fuzzySubtypes.forEach loop more than once).
+        if (needToCheckFuzzySubtypes &&
+            // Start checking fuzzy subtypes only after exhausting all
+            // non-fuzzy subtypes (after the final iteration of the loop).
+            i === workQueue.length - 1 &&
+            // We could wait to compare fragment.selectionSet to result
+            // after we verify the supertype, but this check is often less
+            // expensive than that search, and we will have to do the
+            // comparison anyway whenever we find a potential match.
+            selectionSetMatchesResult(fragment.selectionSet, result!, variables)) {
+          // We don't always need to check fuzzy subtypes (if no result
+          // was provided, or !this.fuzzySubtypes.size), but, when we do,
+          // we only want to check them once.
           needToCheckFuzzySubtypes = false;
-
-          // Now that we're checking fuzzy subtypes, enforce heuristic
-          // fragment/result matching before returning true if/when we
-          // find a matching supertype.
-          mustVerifyResult = true;
+          checkingFuzzySubtypes = true;
 
           // If we find any fuzzy subtypes that match typename, extend the
           // workQueue to search through the supertypes of those fuzzy

From ae4a6f5148448dad549d62b234c9a24c4d97b010 Mon Sep 17 00:00:00 2001
From: Ben Newman <ben@apollographql.com>
Date: Thu, 10 Sep 2020 11:33:04 -0400
Subject: [PATCH 6/7] Add more tests of possibleTypes and fragmentMatches.

---
 .../__snapshots__/fragmentMatcher.ts.snap     |  29 ++
 .../inmemory/__tests__/fragmentMatcher.ts     | 338 ++++++++++++++++++
 2 files changed, 367 insertions(+)
 create mode 100644 src/cache/inmemory/__tests__/__snapshots__/fragmentMatcher.ts.snap

diff --git a/src/cache/inmemory/__tests__/__snapshots__/fragmentMatcher.ts.snap b/src/cache/inmemory/__tests__/__snapshots__/fragmentMatcher.ts.snap
new file mode 100644
index 00000000000..0b0c04b9abf
--- /dev/null
+++ b/src/cache/inmemory/__tests__/__snapshots__/fragmentMatcher.ts.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`policies.fragmentMatches can infer fuzzy subtypes heuristically 1`] = `
+Object {
+  "ROOT_QUERY": Object {
+    "__typename": "Query",
+    "objects": Array [
+      Object {
+        "__typename": "E",
+        "c": "ce",
+      },
+      Object {
+        "__typename": "F",
+        "c": "cf",
+      },
+      Object {
+        "__typename": "G",
+        "c": "cg",
+      },
+      Object {
+        "__typename": "TooLong",
+      },
+      Object {
+        "__typename": "H",
+      },
+    ],
+  },
+}
+`;
diff --git a/src/cache/inmemory/__tests__/fragmentMatcher.ts b/src/cache/inmemory/__tests__/fragmentMatcher.ts
index 279a42ea902..d4062f64d97 100644
--- a/src/cache/inmemory/__tests__/fragmentMatcher.ts
+++ b/src/cache/inmemory/__tests__/fragmentMatcher.ts
@@ -1,6 +1,8 @@
 import gql from 'graphql-tag';
 
 import { InMemoryCache } from '../inMemoryCache';
+import { visit, FragmentDefinitionNode } from 'graphql';
+import { hasOwn } from '../helpers';
 
 describe('fragment matching', () => {
   it('can match exact types with or without possibleTypes', () => {
@@ -222,4 +224,340 @@ describe('fragment matching', () => {
     cache.writeQuery({ query, data });
     expect(cache.readQuery({ query })).toEqual(data);
   });
+
+});
+
+describe("policies.fragmentMatches", () => {
+  const warnings: any[] = [];
+  const { warn } = console;
+
+  beforeEach(() => {
+    warnings.length = 0;
+    console.warn = function (message: any) {
+      warnings.push(message);
+    };
+  });
+
+  afterEach(() => {
+    console.warn = warn;
+  });
+
+  it("can infer fuzzy subtypes heuristically", () => {
+    const cache = new InMemoryCache({
+      possibleTypes: {
+        A: ["B", "C"],
+        B: ["D"],
+        C: ["[E-Z]"],
+      },
+    });
+
+    const fragments = gql`
+      fragment FragA on A { a }
+      fragment FragB on B { b }
+      fragment FragC on C { c }
+      fragment FragD on D { d }
+      fragment FragE on E { e }
+      fragment FragF on F { f }
+    `;
+
+    function checkTypes(
+      expected: Record<string, Record<string, boolean>>,
+    ) {
+      const checked = new Set<FragmentDefinitionNode>();
+
+      visit(fragments, {
+        FragmentDefinition(frag) {
+          function check(typename: string, result: boolean) {
+            if (result !== cache.policies.fragmentMatches(frag, typename)) {
+              fail(`fragment ${
+                frag.name.value
+              } should${result ? "" : " not"} have matched typename ${typename}`);
+            }
+          }
+
+          const supertype = frag.typeCondition.name.value;
+          expect("ABCDEF".split("")).toContain(supertype);
+
+          if (hasOwn.call(expected, supertype)) {
+            Object.keys(expected[supertype]).forEach(subtype => {
+              check(subtype, expected[supertype][subtype]);
+            });
+
+            checked.add(frag);
+          }
+        },
+      });
+
+      return checked;
+    }
+
+    expect(checkTypes({
+      A: {
+        A: true,
+        B: true,
+        C: true,
+        D: true,
+        E: false,
+        F: false,
+        G: false,
+      },
+      B: {
+        A: false,
+        B: true,
+        C: false,
+        D: true,
+        E: false,
+        F: false,
+        G: false,
+      },
+      C: {
+        A: false,
+        B: false,
+        C: true,
+        D: false,
+        E: false,
+        F: false,
+        G: false,
+      },
+      D: {
+        A: false,
+        B: false,
+        C: false,
+        D: true,
+        E: false,
+        F: false,
+        G: false,
+      },
+      E: {
+        A: false,
+        B: false,
+        C: false,
+        D: false,
+        E: true,
+        F: false,
+        G: false,
+      },
+      F: {
+        A: false,
+        B: false,
+        C: false,
+        D: false,
+        E: false,
+        F: true,
+        G: false,
+      },
+      G: {
+        A: false,
+        B: false,
+        C: false,
+        D: false,
+        E: false,
+        F: false,
+        G: true,
+      },
+    }).size).toBe("ABCDEF".length);
+
+    cache.writeQuery({
+      query: gql`
+        query {
+          objects {
+            ...FragC
+          }
+        }
+        ${fragments}
+      `,
+      data: {
+        objects: [
+          { __typename: "E", c: "ce" },
+          { __typename: "F", c: "cf" },
+          { __typename: "G", c: "cg" },
+          // The /[E-Z]/ subtype pattern specified for the C supertype
+          // must match the entire subtype string.
+          { __typename: "TooLong", c: "nope" },
+          // The H typename matches the regular expression for C, but it
+          // does not pass the heuristic test of having all the fields
+          // expected if FragC matched.
+          { __typename: "H", h: "not c" },
+        ],
+      },
+    });
+
+    expect(warnings).toEqual([
+      "Inferring subtype E of supertype C",
+      "Inferring subtype F of supertype C",
+      "Inferring subtype G of supertype C",
+      // Note that TooLong is not inferred here.
+    ]);
+
+    expect(checkTypes({
+      A: {
+        A: true,
+        B: true,
+        C: true,
+        D: true,
+        E: true,
+        F: true,
+        G: true,
+        H: false,
+      },
+      B: {
+        A: false,
+        B: true,
+        C: false,
+        D: true,
+        E: false,
+        F: false,
+        G: false,
+        H: false,
+      },
+      C: {
+        A: false,
+        B: false,
+        C: true,
+        D: false,
+        E: true,
+        F: true,
+        G: true,
+        H: false,
+      },
+      D: {
+        A: false,
+        B: false,
+        C: false,
+        D: true,
+        E: false,
+        F: false,
+        G: false,
+        H: false,
+      },
+      E: {
+        A: false,
+        B: false,
+        C: false,
+        D: false,
+        E: true,
+        F: false,
+        G: false,
+        H: false,
+      },
+      F: {
+        A: false,
+        B: false,
+        C: false,
+        D: false,
+        E: false,
+        F: true,
+        G: false,
+        H: false,
+      },
+      G: {
+        A: false,
+        B: false,
+        C: false,
+        D: false,
+        E: false,
+        F: true,
+        G: true,
+        H: false,
+      },
+    }).size).toBe("ABCDEF".length);
+
+    expect(cache.extract()).toMatchSnapshot();
+
+    // Now add the TooLong subtype of C explicitly.
+    cache.policies.addPossibleTypes({
+      C: ["TooLong"],
+    });
+
+    expect(checkTypes({
+      A: {
+        A: true,
+        B: true,
+        C: true,
+        D: true,
+        E: true,
+        F: true,
+        G: true,
+        TooLong: true,
+        H: false,
+      },
+      B: {
+        A: false,
+        B: true,
+        C: false,
+        D: true,
+        E: false,
+        F: false,
+        G: false,
+        TooLong: false,
+        H: false,
+      },
+      C: {
+        A: false,
+        B: false,
+        C: true,
+        D: false,
+        E: true,
+        F: true,
+        G: true,
+        TooLong: true,
+        H: false,
+      },
+      D: {
+        A: false,
+        B: false,
+        C: false,
+        D: true,
+        E: false,
+        F: false,
+        G: false,
+        TooLong: false,
+        H: false,
+      },
+      E: {
+        A: false,
+        B: false,
+        C: false,
+        D: false,
+        E: true,
+        F: false,
+        G: false,
+        TooLong: false,
+        H: false,
+      },
+      F: {
+        A: false,
+        B: false,
+        C: false,
+        D: false,
+        E: false,
+        F: true,
+        G: false,
+        TooLong: false,
+        H: false,
+      },
+      G: {
+        A: false,
+        B: false,
+        C: false,
+        D: false,
+        E: false,
+        F: true,
+        G: true,
+        TooLong: false,
+        H: false,
+      },
+      H: {
+        A: false,
+        B: false,
+        C: false,
+        D: false,
+        E: false,
+        F: false,
+        G: false,
+        TooLong: false,
+        H: true,
+      },
+    }).size).toBe("ABCDEF".length);
+  });
 });

From 02b0ac84e9c7b81cfcc37b82d625ebf6699984a1 Mon Sep 17 00:00:00 2001
From: Ben Newman <ben@apollographql.com>
Date: Thu, 10 Sep 2020 14:06:24 -0400
Subject: [PATCH 7/7] Mention PR #6901 in CHANGELOG.md.

---
 CHANGELOG.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index fcd3aff8bee..28e7a425e1a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,9 @@
 - Substantially improve type inference for data and variables types using `TypedDocumentNode<Data, Variables>` type instead of `DocumentNode`. <br/>
   [@dotansimha](https://github.com/dotansimha) in [#6720](https://github.com/apollographql/apollo-client/pull/6720)
 
+- Bring back an improved form of heuristic fragment matching, by allowing `possibleTypes` to specify subtype regular expression strings, which count as matches if the written result object has all the fields expected for the fragment. <br/>
+  [@benjamn](https://github.com/benjamn) in [#6901](https://github.com/apollographql/apollo-client/pull/6901)
+
 - Allow `options.nextFetchPolicy` to be a function that takes the current `FetchPolicy` and returns a new (or the same) `FetchPolicy`, making `nextFetchPolicy` more suitable for global use in `defaultOptions.watchQuery`. <br/>
   [@benjamn](https://github.com/benjamn) in [#6893](https://github.com/apollographql/apollo-client/pull/6893)