diff --git a/frontend/src/metabase-lib/lib/Dimension.ts b/frontend/src/metabase-lib/lib/Dimension.ts
index 06948bdb717d9749095da9ac9d92aec339fbf06a..c8165cc62f465486f41ef50b91183b5a9df40d1c 100644
--- a/frontend/src/metabase-lib/lib/Dimension.ts
+++ b/frontend/src/metabase-lib/lib/Dimension.ts
@@ -2,7 +2,6 @@
 // @ts-nocheck
 import { t, ngettext, msgid } from "ttag";
 import _ from "underscore";
-import { merge } from "icepick";
 import { stripId, FK_SYMBOL } from "metabase/lib/formatting";
 import { TYPE } from "metabase/lib/types";
 import { TemplateTagVariable } from "metabase-lib/lib/Variable";
@@ -27,7 +26,6 @@ import ValidationError, {
 import { IconName } from "metabase-types/types";
 import { getFieldValues, getRemappings } from "metabase/lib/query/field";
 import { DATETIME_UNITS, formatBucketing } from "metabase/lib/query_time";
-import { getQuestionIdFromVirtualTableId } from "metabase/lib/saved-questions";
 import Aggregation from "metabase-lib/lib/queries/structured/Aggregation";
 import Filter from "metabase-lib/lib/queries/structured/Filter";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
@@ -796,36 +794,13 @@ export class FieldDimension extends Dimension {
     return typeof this._fieldIdOrName === "string";
   }
 
-  _createField(fieldInfo, { hydrate = false } = {}): Field {
+  _createField(fieldInfo): Field {
     const field = new Field({
       ...fieldInfo,
       metadata: this._metadata,
       query: this._query,
     });
 
-    // This is normally done when calculating metadata,
-    // but since we're merging plain objects without these fields,
-    // we need to repeat the hydration again.
-    // We should definitely move it out of there
-    if (hydrate) {
-      field.table =
-        this.query()?.table() ?? this._metadata.table(field.table_id);
-      field.table_id = field.table?.id;
-
-      if (field.isFK()) {
-        field.target = this._metadata.field(field.fk_target_field_id);
-      }
-
-      if (field.name_field != null) {
-        field.field_name = this._metadata.field(field.name_field);
-      } else if (field.table && field.isPK()) {
-        field.field_name = _.find(field.table.fields, f => f.isEntityName());
-      }
-
-      field.values = getFieldValues(field);
-      field.remappings = new Map(getRemappings(field));
-    }
-
     return field;
   }
 
@@ -842,120 +817,55 @@ export class FieldDimension extends Dimension {
     }
   }
 
-  _getFieldMetadataFromSavedDataset() {
-    const identifierProp = this._getIdentifierProp();
-    const questionAssociatedWithDimension = this.query()?.question();
-    if (
-      questionAssociatedWithDimension?.isSaved() &&
-      questionAssociatedWithDimension.isDataset()
-    ) {
-      const fieldMetadata = _.findWhere(
-        questionAssociatedWithDimension.getResultMetadata(),
-        {
-          [identifierProp]: this.fieldIdOrName(),
-        },
-      );
-
-      return fieldMetadata;
-    }
-  }
-
-  _getFieldMetadataFromNestedDataset() {
+  _findMatchingQueryField() {
     const identifierProp = this._getIdentifierProp();
-    const virtualTableCardId = getQuestionIdFromVirtualTableId(
-      this.query()?.sourceTableId?.(),
-    );
-    if (virtualTableCardId != null) {
-      const question = this._metadata?.question(virtualTableCardId);
-      const fieldMetadata = question?.isDataset()
-        ? _.findWhere(question.getResultMetadata(), {
-            [identifierProp]: this.fieldIdOrName(),
-          })
-        : undefined;
-
-      return fieldMetadata;
+    const fieldIdentifier = this.fieldIdOrName();
+    if (this._query) {
+      const queryTableFields = this._query.table()?.fields;
+      return _.findWhere(queryTableFields, {
+        [identifierProp]: fieldIdentifier,
+      });
     }
   }
 
-  _getFieldMetadataFromQueryTable() {
-    const identifierProp = this._getIdentifierProp();
-    const table = this.query()?.table();
-    return _.findWhere(table?.fields, {
-      [identifierProp]: this.fieldIdOrName(),
+  _createFallbackField(): Field {
+    return this._createField({
+      id: this.isIntegerFieldId() ? this.fieldIdOrName() : this.mbql(),
+      field_ref: this.mbql(),
+      name: this.isStringFieldName() && this.fieldIdOrName(),
+      display_name: this.fieldIdOrName(),
+      base_type: this.getOption("base-type"),
     });
   }
 
-  _combineFieldWithExtraMetadata(field: Field | undefined, fieldMetadata) {
-    if (field) {
-      if (!fieldMetadata || field === fieldMetadata) {
-        return field;
-      }
-
-      const fieldObject = merge(
-        field instanceof Field ? field.getPlainObject() : field,
-        fieldMetadata instanceof Field
-          ? fieldMetadata.getPlainObject()
-          : fieldMetadata,
-      );
-      return this._createField(fieldObject, { hydrate: true });
-    }
-
-    if (fieldMetadata) {
-      if (fieldMetadata instanceof Field) {
-        return fieldMetadata;
-      }
-
-      return this._createField(fieldMetadata);
-    }
-  }
-
   field(): Field {
     // If a Field is cached on the FieldDimension instance, we can shortwire this method and
     // return the cached Field.
-    const cachedField = this._getTrustedFieldCachedOnInstance();
-    if (cachedField) {
-      return cachedField;
-    }
-
-    let fieldMetadata;
-
-    // Models contain custom metadata for fields, but when these fields have integer ids,
-    // they are clobbered by the metadata from the real table that the card is based on,
-    // so we need to check the result_metadata of the nested card and merge it with the
-    // Field that exists in the Metadata object.
-    if (this.isIntegerFieldId()) {
-      fieldMetadata =
-        this._getFieldMetadataFromSavedDataset() ||
-        this._getFieldMetadataFromNestedDataset();
-    }
-
-    // In scenarios where the Field id is not an integer, we need to grab the Field from the
-    // query's table, because the Field won't exist in the Metadata object (and string Field ids
-    // are not sufficiently unique to be stored properly in the Metadata object).
-    fieldMetadata = fieldMetadata || this._getFieldMetadataFromQueryTable();
-
-    // The `fieldMetadata` object may have metadata that overrides the regular field object
-    // (e.g. a custom field display name or description on a model)
-    const field = this._metadata?.field(this.fieldIdOrName());
-    const combinedField = this._combineFieldWithExtraMetadata(
-      field,
-      fieldMetadata,
-    );
+    const locallyCachedField = this._getTrustedFieldCachedOnInstance();
+    if (locallyCachedField) {
+      return locallyCachedField;
+    }
+
+    // Prioritize pulling a `field` from the Dimenion's associated query (if one exists)
+    // because it might have locally overriding metadata on it.
+    const fieldFromQuery = this._findMatchingQueryField();
+    if (fieldFromQuery) {
+      return fieldFromQuery;
+    }
 
-    if (combinedField) {
-      return combinedField;
+    const maybeTableId = this._query?.table()?.id;
+    const fieldFromGlobalState =
+      this._metadata?.field(this.fieldIdOrName(), maybeTableId) ||
+      this._metadata?.field(this.fieldIdOrName());
+    if (fieldFromGlobalState) {
+      return fieldFromGlobalState;
     }
 
+    // Hitting this return statement means that there is a bug.
+    // This primarily serves as a way to guarantee that this function returns a Field to avoid errors in dependent code.
     // Despite being unable to find a field, we _might_ still have enough data to know a few things about it.
     // For example, if we have an mbql field reference, it might contain a `base-type`
-    return this._createField({
-      id: this.isIntegerFieldId() ? this.fieldIdOrName() : this.mbql(),
-      name: this.isStringFieldName() && this.fieldIdOrName(),
-      // NOTE: this display_name will likely be incorrect
-      // if a `FieldDimension` isn't associated with a query then we don't know which table it belongs to
-      display_name: this._fieldIdOrName,
-      base_type: this.getOption("base-type"),
-    });
+    return this._createFallbackField();
   }
 
   tableId() {
diff --git a/frontend/src/metabase-lib/lib/Question.ts b/frontend/src/metabase-lib/lib/Question.ts
index 7d534e5240878472881d4d848c43d9bd50f2bee5..d05e0cdacb871fe4091dfe69c1b0f31471c2a509 100644
--- a/frontend/src/metabase-lib/lib/Question.ts
+++ b/frontend/src/metabase-lib/lib/Question.ts
@@ -75,6 +75,7 @@ import {
 } from "metabase-lib/lib/Alert";
 import { utf8_to_b64url } from "metabase/lib/encoding";
 import { CollectionId } from "metabase-types/api";
+import { getQuestionVirtualTableId } from "metabase/lib/saved-questions/saved-questions";
 
 type QuestionUpdateFn = (q: Question) => Promise<void> | null | undefined;
 
@@ -583,7 +584,7 @@ class QuestionInner {
           type: "query",
           database: this.databaseId(),
           query: {
-            "source-table": "card__" + this.id(),
+            "source-table": getQuestionVirtualTableId(this.card()),
           },
         },
       };
@@ -600,7 +601,7 @@ class QuestionInner {
       type: "query",
       database: this.databaseId(),
       query: {
-        "source-table": "card__" + this.id(),
+        "source-table": getQuestionVirtualTableId(this.card()),
       },
     });
   }
@@ -1021,11 +1022,18 @@ class QuestionInner {
   }
 
   dependentMetadata(): DependentMetadataItem[] {
-    if (!this.isDataset()) {
-      return [];
-    }
     const dependencies = [];
 
+    // we frequently treat dataset/model questions like they are already nested
+    // so we need to fetch the virtual card table representation of the Question
+    // so that we can properly access the table's fields in various scenarios
+    if (this.isDataset()) {
+      dependencies.push({
+        type: "table",
+        id: getQuestionVirtualTableId(this.card()),
+      });
+    }
+
     this.getResultMetadata().forEach(field => {
       if (isFK(field) && field.fk_target_field_id) {
         dependencies.push({
diff --git a/frontend/src/metabase-lib/lib/metadata/Base.ts b/frontend/src/metabase-lib/lib/metadata/Base.ts
index 52b03b42b22962bbabd91cbad59107f9656b143c..9285b69350ec2734e3acc60975dba6ce37ee3887 100644
--- a/frontend/src/metabase-lib/lib/metadata/Base.ts
+++ b/frontend/src/metabase-lib/lib/metadata/Base.ts
@@ -1,7 +1,7 @@
 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
 // @ts-nocheck
 export default class Base {
-  _plainObject = null;
+  _plainObject: Record<string, unknown>;
 
   constructor(object = {}) {
     this._plainObject = object;
diff --git a/frontend/src/metabase-lib/lib/metadata/Field.ts b/frontend/src/metabase-lib/lib/metadata/Field.ts
index 74707a42e5fb5bb84c7ecfac259163451b03b913..c1a4bc07911822d8fd3f1adc67fa11329a373ce6 100644
--- a/frontend/src/metabase-lib/lib/metadata/Field.ts
+++ b/frontend/src/metabase-lib/lib/metadata/Field.ts
@@ -2,6 +2,7 @@
 // @ts-nocheck
 import _ from "underscore";
 import moment from "moment-timezone";
+
 import { createLookupByProperty, memoizeClass } from "metabase-lib/lib/utils";
 import { formatField, stripId } from "metabase/lib/formatting";
 import { getFieldValues } from "metabase/lib/query/field";
@@ -40,6 +41,7 @@ import type StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 import type NativeQuery from "metabase-lib/lib/queries/NativeQuery";
 import type Table from "./Table";
 import type Metadata from "./Metadata";
+import { getUniqueFieldId } from "./utils";
 
 export const LONG_TEXT_MIN = 80;
 
@@ -65,6 +67,7 @@ class FieldInner extends Base {
   has_field_values?: "list" | "search" | "none";
   values: any[];
   metadata?: Metadata;
+  source?: string;
 
   // added when creating "virtual fields" that are associated with a given query
   query?: StructuredQuery | NativeQuery;
@@ -77,6 +80,19 @@ class FieldInner extends Base {
     return this.id;
   }
 
+  // `uniqueId` is set by our normalizr schema so it is not always available,
+  // if the Field instance was instantiated outside of an entity
+  getUniqueId() {
+    if (this.uniqueId) {
+      return this.uniqueId;
+    }
+
+    const uniqueId = getUniqueFieldId(this.getId(), this.table_id);
+    this.uniqueId = uniqueId;
+
+    return uniqueId;
+  }
+
   parent() {
     return this.metadata ? this.metadata.field(this.parent_id) : null;
   }
@@ -263,6 +279,10 @@ class FieldInner extends Base {
     }
   }
 
+  // 1. `_fieldInstance` is passed in so that we can shortwire any subsequent calls to `field()` form the dimension instance
+  // 2. The distinction between "fields" and "dimensions" is fairly fuzzy, and this method is "wrong" in the sense that
+  // The `ref` of this Field instance MIGHT be something like ["aggregation", "count"] which means that we should
+  // instantiate an AggregationDimension, not a FieldDimension, but there are bugs with that route, and this seems to work for now...
   dimension() {
     const ref = this.reference();
     const fieldDimension = new FieldDimension(
@@ -420,6 +440,18 @@ class FieldInner extends Base {
     });
   }
 
+  clone(fieldMetadata) {
+    if (fieldMetadata instanceof Field) {
+      throw new Error("`fieldMetadata` arg must be a plain object");
+    }
+
+    const plainObject = this.getPlainObject();
+    const newField = new Field({ ...this, ...fieldMetadata });
+    newField._plainObject = { ...plainObject, ...fieldMetadata };
+
+    return newField;
+  }
+
   /**
    * Returns a FKDimension for this field and the provided field
    * @param {Field} foreignField
diff --git a/frontend/src/metabase-lib/lib/metadata/Field.unit.spec.ts b/frontend/src/metabase-lib/lib/metadata/Field.unit.spec.ts
index da0ad1c9ea170848cc2c2e06df46cbf2bf3030ea..b3e766848d16359bc6f71670d4abcfad8d02c4c6 100644
--- a/frontend/src/metabase-lib/lib/metadata/Field.unit.spec.ts
+++ b/frontend/src/metabase-lib/lib/metadata/Field.unit.spec.ts
@@ -6,11 +6,14 @@ import Schema from "./Schema";
 import Metadata from "./Metadata";
 import Base from "./Base";
 import Dimension from "../Dimension";
+import { createMockConcreteField } from "./mocks";
+
 describe("Field", () => {
   describe("instantiation", () => {
     it("should create an instance of Schema", () => {
       expect(new Field()).toBeInstanceOf(Field);
     });
+
     it("should add `object` props to the instance (because it extends Base)", () => {
       expect(new Field()).toBeInstanceOf(Base);
       expect(
@@ -20,10 +23,12 @@ describe("Field", () => {
       ).toHaveProperty("foo", "bar");
     });
   });
+
   describe("parent", () => {
     it("should return null when `metadata` does not exist on instance", () => {
       expect(new Field().parent()).toBeNull();
     });
+
     it("should return the field that matches the instance's `parent_id` when `metadata` exists on the instance", () => {
       const parentField = new Field({
         id: 1,
@@ -41,6 +46,7 @@ describe("Field", () => {
       expect(field.parent()).toBe(parentField);
     });
   });
+
   describe("path", () => {
     it("should return list of fields starting with instance, ending with root parent", () => {
       const rootField = new Field({
@@ -66,6 +72,7 @@ describe("Field", () => {
       expect(field.path()).toEqual([rootField, parentField, field]);
     });
   });
+
   describe("displayName", () => {
     it("should return a field's display name", () => {
       expect(
@@ -74,6 +81,7 @@ describe("Field", () => {
         }).displayName(),
       ).toBe("foo");
     });
+
     it("should prioritize the `display_name` field over `name`", () => {
       expect(
         new Field({
@@ -82,6 +90,7 @@ describe("Field", () => {
         }).displayName(),
       ).toBe("bar");
     });
+
     it("should prioritize the name in the field's `dimensions` property if it has one", () => {
       const field = new Field({
         dimensions: {
@@ -91,6 +100,7 @@ describe("Field", () => {
       });
       expect(field.displayName()).toBe("dimensions");
     });
+
     describe("includePath flag", () => {
       let field;
       beforeEach(() => {
@@ -118,6 +128,7 @@ describe("Field", () => {
           name: "field",
         });
       });
+
       it("should add parent field display names to the field's display name when enabled", () => {
         expect(
           field.displayName({
@@ -125,6 +136,7 @@ describe("Field", () => {
           }),
         ).toBe("rootField: parentField: field");
       });
+
       it("should be enabled by default", () => {
         expect(
           field.displayName({
@@ -132,6 +144,7 @@ describe("Field", () => {
           }),
         ).toBe(field.displayName());
       });
+
       it("should exclude parent field display names when disabled", () => {
         expect(
           field.displayName({
@@ -140,6 +153,7 @@ describe("Field", () => {
         ).toBe("field");
       });
     });
+
     describe("includeTable flag", () => {
       let field;
       beforeEach(() => {
@@ -148,6 +162,7 @@ describe("Field", () => {
           name: "field",
         });
       });
+
       it("should do nothing when there is no table on the field instance", () => {
         expect(
           field.displayName({
@@ -155,6 +170,7 @@ describe("Field", () => {
           }),
         ).toBe("field");
       });
+
       it("should add the table name to the start of the field name", () => {
         field.table = new Table({
           display_name: "table",
@@ -166,6 +182,7 @@ describe("Field", () => {
         ).toBe("table → field");
       });
     });
+
     describe("includeSchema flag", () => {
       let field;
       beforeEach(() => {
@@ -174,6 +191,7 @@ describe("Field", () => {
           name: "field",
         });
       });
+
       it("won't do anything if enabled and includeTable is not enabled", () => {
         expect(
           field.displayName({
@@ -181,6 +199,7 @@ describe("Field", () => {
           }),
         ).toBe("field");
       });
+
       it("should add a combined schema + table display name to the start of the field name", () => {
         field.table = new Table({
           display_name: "table",
@@ -197,6 +216,7 @@ describe("Field", () => {
       });
     });
   });
+
   describe("targetObjectName", () => {
     it("should return the display name of the field stripped of an appended id", () => {
       const field = new Field({
@@ -205,6 +225,7 @@ describe("Field", () => {
       expect(field.targetObjectName()).toBe("field");
     });
   });
+
   describe("dimension", () => {
     it("should return the field's dimension when the id is an mbql field", () => {
       const field = new Field({
@@ -214,6 +235,7 @@ describe("Field", () => {
       expect(dimension).toBeInstanceOf(Dimension);
       expect(dimension.fieldIdOrName()).toBe(123);
     });
+
     it("should return the field's dimension when the id is not an mbql field", () => {
       const field = new Field({
         id: 123,
@@ -223,6 +245,7 @@ describe("Field", () => {
       expect(dimension.fieldIdOrName()).toBe(123);
     });
   });
+
   describe("getDefaultDateTimeUnit", () => {
     describe("when the field is of type `type/DateTime`", () => {
       it("should return 'day'", () => {
@@ -237,6 +260,7 @@ describe("Field", () => {
       });
     });
   });
+
   describe("when field is of type `type/DateTime`", () => {
     it("should return a time unit depending on the number of days in the 'fingerprint'", () => {
       const field = new Field({
@@ -252,6 +276,7 @@ describe("Field", () => {
       expect(field.getDefaultDateTimeUnit()).toBe("month");
     });
   });
+
   describe("remappedField", () => {
     it("should return the 'human readable' field tied to the field's dimension", () => {
       const field1 = new Field({
@@ -275,6 +300,7 @@ describe("Field", () => {
       field.metadata = metadata;
       expect(field.remappedField()).toBe(field1);
     });
+
     it("should return the field's name_field", () => {
       const nameField = new Field();
       const field = new Field({
@@ -283,10 +309,12 @@ describe("Field", () => {
       });
       expect(field.remappedField()).toBe(nameField);
     });
+
     it("should return null when the field has no name_field or no dimension with a 'human readable' field", () => {
       expect(new Field().remappedField()).toBe(null);
     });
   });
+
   describe("remappedValue", () => {
     it("should call a given value using the instance's remapping property", () => {
       const field = new Field({
@@ -296,6 +324,7 @@ describe("Field", () => {
       });
       expect(field.remappedValue(2)).toBe(1);
     });
+
     it("should convert a numeric field into a number if it is not a number", () => {
       const field = new Field({
         isNumeric: () => true,
@@ -306,6 +335,7 @@ describe("Field", () => {
       expect(field.remappedValue("2.5rem")).toBe(2.5);
     });
   });
+
   describe("hasRemappedValue", () => {
     it("should call a given value using the instance's remapping property", () => {
       const field = new Field({
@@ -315,6 +345,7 @@ describe("Field", () => {
       });
       expect(field.hasRemappedValue(2)).toBe(true);
     });
+
     it("should convert a numeric field into a number if it is not a number", () => {
       const field = new Field({
         isNumeric: () => true,
@@ -325,6 +356,7 @@ describe("Field", () => {
       expect(field.hasRemappedValue("2.5rem")).toBe(true);
     });
   });
+
   describe("isSearchable", () => {
     it("should be true when the field is a string", () => {
       const field = new Field({
@@ -339,6 +371,7 @@ describe("Field", () => {
       expect(field.isSearchable()).toBe(false);
     });
   });
+
   describe("fieldValues", () => {
     it("should return the values on a field instance", () => {
       const values = [[1], [2]];
@@ -347,6 +380,7 @@ describe("Field", () => {
       });
       expect(field.fieldValues()).toEqual(values);
     });
+
     it("should wrap raw values in arrays to match the format of remapped values", () => {
       const values = [1, 2];
       const field = new Field({
@@ -355,6 +389,7 @@ describe("Field", () => {
       expect(field.fieldValues()).toEqual([[1], [2]]);
     });
   });
+
   describe("hasFieldValues", () => {
     it("should be true when a field has values", () => {
       expect(
@@ -363,6 +398,7 @@ describe("Field", () => {
         }).hasFieldValues(),
       ).toBe(true);
     });
+
     it("should be false when a field has no values", () => {
       expect(
         new Field({
@@ -376,4 +412,57 @@ describe("Field", () => {
       ).toBe(false);
     });
   });
+
+  describe("getUniqueId", () => {
+    describe("when the `uniqueId` field exists on the instance", () => {
+      it("should return the `uniqueId`", () => {
+        const field = new Field({
+          uniqueId: "foo",
+        });
+        expect(field.getUniqueId()).toBe("foo");
+      });
+    });
+
+    describe("when the `uniqueId` field does not exist on the instance of a concrete Field", () => {
+      let field;
+      beforeEach(() => {
+        field = createMockConcreteField({
+          apiOpts: {
+            id: 1,
+            table_id: 2,
+          },
+        });
+      });
+
+      it("should create a `uniqueId`", () => {
+        expect(field.getUniqueId()).toBe(1);
+      });
+
+      it("should set the `uniqueId` on the Field instance", () => {
+        field.getUniqueId();
+        expect(field.uniqueId).toBe(1);
+      });
+    });
+
+    describe("when the `uniqueId` field does not exist on the instance of a Field from a virtual card Table", () => {
+      let field;
+      beforeEach(() => {
+        field = createMockConcreteField({
+          apiOpts: {
+            id: 1,
+            table_id: "card__123",
+          },
+        });
+      });
+
+      it("should create a `uniqueId`", () => {
+        expect(field.getUniqueId()).toBe("card__123:1");
+      });
+
+      it("should set the `uniqueId` on the Field instance", () => {
+        field.getUniqueId();
+        expect(field.uniqueId).toBe("card__123:1");
+      });
+    });
+  });
 });
diff --git a/frontend/src/metabase-lib/lib/metadata/Metadata.ts b/frontend/src/metabase-lib/lib/metadata/Metadata.ts
index ded4bbafb41047abd0f12aa7f5adee33b2e54f80..349ee94cec73527ebe63db1f9b2a8a0c40e8d4dd 100644
--- a/frontend/src/metabase-lib/lib/metadata/Metadata.ts
+++ b/frontend/src/metabase-lib/lib/metadata/Metadata.ts
@@ -6,6 +6,8 @@ import type Question from "../Question";
 import type Database from "./Database";
 import type Table from "./Table";
 import type Schema from "./Schema";
+import type Field from "./Field";
+import { getUniqueFieldId } from "./utils";
 
 /**
  * @typedef { import("./metadata").DatabaseId } DatabaseId
@@ -102,12 +104,16 @@ export default class Metadata extends Base {
     return (tableId != null && this.tables[tableId]) || null;
   }
 
-  /**
-   * @param {FieldId} fieldId
-   * @returns {?Field}
-   */
-  field(fieldId): Field | null {
-    return (fieldId != null && this.fields[fieldId]) || null;
+  field(
+    fieldId: Field["id"] | undefined | null,
+    tableId?: Table["id"] | undefined | null,
+  ): Field | null {
+    if (fieldId == null) {
+      return null;
+    }
+
+    const uniqueId = getUniqueFieldId(fieldId, tableId);
+    return this.fields[uniqueId] || null;
   }
 
   question(cardId): Question | null {
diff --git a/frontend/src/metabase-lib/lib/metadata/Metadata.unit.spec.ts b/frontend/src/metabase-lib/lib/metadata/Metadata.unit.spec.ts
index 9149f4eeb3dd99759983e8094a7b4a936ce19cb2..c535c37566beb6f82f07b1b326ea444d543bb1bb 100644
--- a/frontend/src/metabase-lib/lib/metadata/Metadata.unit.spec.ts
+++ b/frontend/src/metabase-lib/lib/metadata/Metadata.unit.spec.ts
@@ -5,10 +5,10 @@ import Base from "./Base";
 import Database from "./Database";
 import Table from "./Table";
 import Schema from "./Schema";
-import Field from "./Field";
 import Segment from "./Segment";
 import Metric from "./Metric";
-import Question from "../Question";
+import { createMockConcreteField } from "./mocks";
+
 describe("Metadata", () => {
   describe("instantiation", () => {
     it("should create an instance of Metadata", () => {
@@ -146,7 +146,6 @@ describe("Metadata", () => {
     ["database", obj => new Database(obj)],
     ["schema", obj => new Schema(obj)],
     ["table", obj => new Table(obj)],
-    ["field", obj => new Field(obj)],
   ].forEach(([fnName, instantiate]) => {
     describe(fnName, () => {
       let instanceA;
@@ -181,4 +180,73 @@ describe("Metadata", () => {
       });
     });
   });
+
+  describe("`field`", () => {
+    it("should return null when given a nil fieldId arg", () => {
+      const metadata = new Metadata({
+        fields: {},
+      });
+      expect(metadata.field()).toBeNull();
+      expect(metadata.field(null)).toBeNull();
+    });
+
+    describe("when given a fieldId and no tableId", () => {
+      it("should return null when there is no matching field", () => {
+        const metadata = new Metadata({
+          fields: {},
+        });
+        expect(metadata.field(1)).toBeNull();
+      });
+
+      it("should return the matching Field instance", () => {
+        const field = createMockConcreteField({ apiOpts: { id: 1 } });
+        const uniqueId = field.getUniqueId();
+        const metadata = new Metadata({
+          fields: {
+            [uniqueId]: field,
+          },
+        });
+
+        expect(metadata.field(1)).toBe(field);
+      });
+    });
+
+    describe("when given a fieldId and a concrete tableId", () => {
+      it("should ignore the tableId arg because these fields are stored using the field's id", () => {
+        const field = createMockConcreteField({
+          apiOpts: { id: 1, table_id: 1 },
+        });
+        const uniqueId = field.getUniqueId();
+        const metadata = new Metadata({
+          fields: {
+            [uniqueId]: field,
+          },
+        });
+
+        expect(metadata.field(1, 1)).toBe(field);
+        // to prove the point that the `tableId` is ignore in this scenario
+        expect(metadata.field(1, 2)).toBe(field);
+        expect(metadata.field(1)).toBe(field);
+      });
+    });
+
+    describe("when given a fieldId and a virtual card tableId", () => {
+      it("should return the matching Field instance, stored using the field's `uniqueId`", () => {
+        const field = createMockConcreteField({
+          apiOpts: { id: 1, table_id: "card__123" },
+        });
+        const uniqueId = field.getUniqueId();
+        const metadata = new Metadata({
+          fields: {
+            [uniqueId]: field,
+          },
+        });
+
+        expect(metadata.field(1, "card__123")).toBe(field);
+        expect(metadata.field("card__123:1")).toBe(field);
+
+        expect(metadata.field(1)).not.toBe(field);
+      });
+    });
+  });
 });
diff --git a/frontend/src/metabase-lib/lib/metadata/Table.ts b/frontend/src/metabase-lib/lib/metadata/Table.ts
index ee924b2cdb68dfd6acadacbce74f8b4816dd57fa..da8692aa5cb305ba13259136d715946631531294 100644
--- a/frontend/src/metabase-lib/lib/metadata/Table.ts
+++ b/frontend/src/metabase-lib/lib/metadata/Table.ts
@@ -142,6 +142,13 @@ class TableInner extends Base {
     return pks;
   }
 
+  clone() {
+    const plainObject = this.getPlainObject();
+    const newTable = new Table(this);
+    newTable._plainObject = plainObject;
+    return newTable;
+  }
+
   /**
    * @private
    * @param {string} description
diff --git a/frontend/src/metabase-lib/lib/metadata/Table.unit.spec.ts b/frontend/src/metabase-lib/lib/metadata/Table.unit.spec.ts
index 5e5490daf026829efce22b7585d476f80b74a4a2..deaa3f030d3eee708419dfa130fca67eb6a5e905 100644
--- a/frontend/src/metabase-lib/lib/metadata/Table.unit.spec.ts
+++ b/frontend/src/metabase-lib/lib/metadata/Table.unit.spec.ts
@@ -1,5 +1,6 @@
 import { PRODUCTS } from "__support__/sample_database_fixture";
 import Table from "./Table";
+import type Field from "./Field";
 
 describe("Table", () => {
   const productsTable = new Table(PRODUCTS);
diff --git a/frontend/src/metabase-lib/lib/metadata/mocks.ts b/frontend/src/metabase-lib/lib/metadata/mocks.ts
new file mode 100644
index 0000000000000000000000000000000000000000..140d22d5c7654f8f9ba1ac333f6eafc42971b2d0
--- /dev/null
+++ b/frontend/src/metabase-lib/lib/metadata/mocks.ts
@@ -0,0 +1,18 @@
+import Field from "./Field";
+import { createMockField } from "metabase-types/api/mocks/field";
+import { Field as ApiField } from "metabase-types/api";
+
+export function createMockConcreteField({
+  apiOpts,
+  instanceOpts,
+}: {
+  apiOpts?: Partial<ApiField>;
+  instanceOpts?: Partial<Field>;
+}) {
+  const mockField = createMockField(apiOpts);
+  const instance = new Field(mockField);
+
+  Object.assign(instance, instanceOpts);
+
+  return instance;
+}
diff --git a/frontend/src/metabase-lib/lib/metadata/utils.ts b/frontend/src/metabase-lib/lib/metadata/utils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b563e89fddd4a2b4155771983a7a5383904673a7
--- /dev/null
+++ b/frontend/src/metabase-lib/lib/metadata/utils.ts
@@ -0,0 +1,11 @@
+import { isVirtualCardId } from "metabase/lib/saved-questions/saved-questions";
+import type Field from "./Field";
+import type Table from "./Table";
+
+export function getUniqueFieldId(id: Field["id"], tableId: Table["id"]) {
+  if (isVirtualCardId(tableId)) {
+    return `${tableId}:${id}`;
+  }
+
+  return id;
+}
diff --git a/frontend/src/metabase-lib/lib/metadata/utils.unit.spec.ts b/frontend/src/metabase-lib/lib/metadata/utils.unit.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..79b666d4a6f4c2bb4887a688097080c9627c3c91
--- /dev/null
+++ b/frontend/src/metabase-lib/lib/metadata/utils.unit.spec.ts
@@ -0,0 +1,19 @@
+import { getUniqueFieldId } from "./utils";
+
+describe("metabase-lib/metadata/utils", () => {
+  describe("getUniqueFieldId", () => {
+    describe("when the given tableId arg is NOT a virtual card table", () => {
+      it("should return the field's id", () => {
+        expect(getUniqueFieldId(1, 2)).toEqual(1);
+        // @ts-expect-error -- testing for when our types fail
+        expect(getUniqueFieldId(1, undefined)).toEqual(1);
+      });
+    });
+
+    describe("when the given tableId arg is a virtual card table", () => {
+      it("should return a string from the combined ids", () => {
+        expect(getUniqueFieldId(1, "card__123")).toEqual("card__123:1");
+      });
+    });
+  });
+});
diff --git a/frontend/src/metabase-lib/lib/queries/NativeQuery.ts b/frontend/src/metabase-lib/lib/queries/NativeQuery.ts
index 445624634c32147809880f10414fd8420ef1c1ed..70a0f1ffb3ad42d7d1211adf7fff4c369945e547 100644
--- a/frontend/src/metabase-lib/lib/queries/NativeQuery.ts
+++ b/frontend/src/metabase-lib/lib/queries/NativeQuery.ts
@@ -29,6 +29,8 @@ import { createTemplateTag } from "metabase-lib/lib/queries/TemplateTag";
 import DimensionOptions from "../DimensionOptions";
 import ValidationError from "metabase-lib/lib/ValidationError";
 
+import { getNativeQueryTable } from "./utils/native-query-table";
+
 type DimensionFilter = (dimension: Dimension) => boolean;
 type VariableFilter = (variable: Variable) => boolean;
 export const NATIVE_QUERY_TEMPLATE: NativeDatasetQuery = {
@@ -189,19 +191,8 @@ export default class NativeQuery extends AtomicQuery {
     );
   }
 
-  table(): Table | null | undefined {
-    const database = this.database();
-    const collection = this.collection();
-
-    if (!database || !collection) {
-      return null;
-    }
-
-    return (
-      _.findWhere(database.tables, {
-        name: collection,
-      }) || null
-    );
+  table(): Table | null {
+    return getNativeQueryTable(this);
   }
 
   queryText(): string {
diff --git a/frontend/src/metabase-lib/lib/queries/StructuredQuery.ts b/frontend/src/metabase-lib/lib/queries/StructuredQuery.ts
index 723e202cffe37889278efdfb187c00dee4893a9d..5000b911bd6580f098a295840cf67703473dad0a 100644
--- a/frontend/src/metabase-lib/lib/queries/StructuredQuery.ts
+++ b/frontend/src/metabase-lib/lib/queries/StructuredQuery.ts
@@ -51,6 +51,12 @@ import Table from "../metadata/Table";
 import Field from "../metadata/Field";
 import { TYPE } from "metabase/lib/types";
 import { fieldRefForColumn } from "metabase/lib/dataset";
+import {
+  isVirtualCardId,
+  getQuestionIdFromVirtualTableId,
+} from "metabase/lib/saved-questions";
+
+import { getStructuredQueryTable } from "./utils/structured-query-table";
 
 type DimensionFilter = (dimension: Dimension) => boolean;
 type FieldFilter = (filter: Field) => boolean;
@@ -326,41 +332,8 @@ class StructuredQueryInner extends AtomicQuery {
   /**
    * @returns the table object, if a table is selected and loaded.
    */
-  table(): Table {
-    const sourceQuery = this.sourceQuery();
-
-    if (sourceQuery) {
-      const fields = sourceQuery.columns().map(column => {
-        const id = [
-          "field",
-          column.name,
-          {
-            "base-type": column.base_type,
-          },
-        ];
-
-        return new Field({
-          ...column,
-          // TODO FIXME -- Do NOT use field-literal unless you're referring to a native query
-          id,
-          source: "fields",
-          // HACK: need to thread the query through to this fake Field
-          query: this,
-        });
-      });
-
-      return new Table({
-        id: this.sourceTableId(),
-        name: "",
-        display_name: "",
-        db: sourceQuery.database(),
-        fields,
-        segments: [],
-        metrics: [],
-      });
-    }
-
-    return this.metadata().table(this.sourceTableId());
+  table(): Table | null {
+    return getStructuredQueryTable(this);
   }
 
   /**
@@ -1402,7 +1375,7 @@ class StructuredQueryInner extends AtomicQuery {
             f.parent_id == null
           );
         })
-        .sortBy(d => d.displayName().toLowerCase())
+        .sortBy(d => d.displayName()?.toLowerCase())
         .sortBy(d => {
           const type = d.field().semantic_type;
           return type === TYPE.PK ? 0 : type === TYPE.Name ? 1 : 2;
@@ -1604,6 +1577,12 @@ class StructuredQueryInner extends AtomicQuery {
    * returns the original Table object at the beginning of the nested queries
    */
   rootTable(): Table {
+    const question = this.question();
+    const questionTableId = question?.tableId();
+    if (questionTableId != null) {
+      return this.metadata().table(questionTableId);
+    }
+
     return this.rootQuery().table();
   }
 
@@ -1675,6 +1654,13 @@ class StructuredQueryInner extends AtomicQuery {
         id: tableId,
         foreignTables,
       });
+
+      if (isVirtualCardId(tableId)) {
+        addDependency({
+          type: "question",
+          id: getQuestionIdFromVirtualTableId(tableId),
+        });
+      }
     }
 
     // any explicitly joined tables
diff --git a/frontend/src/metabase-lib/lib/queries/structured/Join.ts b/frontend/src/metabase-lib/lib/queries/structured/Join.ts
index 03b255000e398de6d4a41c969ea1a1fd504f7ace..f38418ba0ad6d95385f666a2db5a7d8155d309a3 100644
--- a/frontend/src/metabase-lib/lib/queries/structured/Join.ts
+++ b/frontend/src/metabase-lib/lib/queries/structured/Join.ts
@@ -559,14 +559,14 @@ export default class Join extends MBQLObjectClause {
     const sourceTable = this.joinSourceTableId();
     const sourceQuery = this.joinSourceQuery();
     return sourceTable
-      ? new StructuredQuery(this.query().question(), {
+      ? new StructuredQuery(this.query().question().setDataset(false), {
           type: "query",
           query: {
             "source-table": sourceTable,
           },
         })
       : sourceQuery
-      ? new StructuredQuery(this.query().question(), {
+      ? new StructuredQuery(this.query().question().setDataset(false), {
           type: "query",
           query: sourceQuery,
         })
diff --git a/frontend/src/metabase-lib/lib/queries/utils/native-query-table.ts b/frontend/src/metabase-lib/lib/queries/utils/native-query-table.ts
new file mode 100644
index 0000000000000000000000000000000000000000..effd43a60ecce2455d4c9c67d749768f040b4b21
--- /dev/null
+++ b/frontend/src/metabase-lib/lib/queries/utils/native-query-table.ts
@@ -0,0 +1,28 @@
+import _ from "underscore";
+
+import type Table from "metabase-lib/lib/metadata/Table";
+
+import type NativeQuery from "../NativeQuery";
+import { getDatasetTable } from "./nested-card-query-table";
+
+export function getNativeQueryTable(nativeQuery: NativeQuery): Table | null {
+  const question = nativeQuery.question();
+  const isDataset = question?.isDataset();
+
+  if (isDataset) {
+    return getDatasetTable(nativeQuery);
+  }
+
+  const database = nativeQuery.database();
+  const collection = nativeQuery.collection();
+  if (database && collection) {
+    return (
+      _.findWhere(database.tables, {
+        name: collection,
+      }) || null
+    );
+  }
+
+  // Native queries aren't always associated with a specific table
+  return null;
+}
diff --git a/frontend/src/metabase-lib/lib/queries/utils/native-query-table.unit.spec.ts b/frontend/src/metabase-lib/lib/queries/utils/native-query-table.unit.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d1488eb52e5c908a2a7642e0b78475c16db1b32d
--- /dev/null
+++ b/frontend/src/metabase-lib/lib/queries/utils/native-query-table.unit.spec.ts
@@ -0,0 +1,100 @@
+import { merge } from "icepick";
+
+import {
+  metadata,
+  PRODUCTS,
+  SAMPLE_DATABASE,
+} from "__support__/sample_database_fixture";
+import Question from "metabase-lib/lib/Question";
+import Table from "metabase-lib/lib/metadata/Table";
+import Field from "metabase-lib/lib/metadata/Field";
+import type NativeQuery from "metabase-lib/lib/queries/NativeQuery";
+
+import { getNativeQueryTable } from "./native-query-table";
+
+const NATIVE_QUERY_CARD = {
+  id: 1,
+  dataset_query: {
+    database: SAMPLE_DATABASE?.id,
+    type: "native",
+    native: {
+      query: "select * from ORDERS where CREATED_AT = {{created}}",
+    },
+  },
+};
+
+describe("metabase-lib/queries/utils/native-query-table", () => {
+  describe("native query associated with a dataset/model question", () => {
+    const PRODUCT_ID_WITH_OVERRIDING_METADATA = new Field({
+      ...PRODUCTS.ID.getPlainObject(),
+      display_name: "~*~Products.ID~*~",
+    });
+
+    const nativeDatasetQuestion = new Question(NATIVE_QUERY_CARD, metadata)
+      .setDataset(true)
+      .setDisplayName("Native Dataset Question");
+
+    const nestedNativeDatasetTable = new Table({
+      id: "card__1",
+      display_name: nativeDatasetQuestion.displayName(),
+      name: nativeDatasetQuestion.displayName(),
+      fields: [PRODUCT_ID_WITH_OVERRIDING_METADATA],
+    });
+
+    metadata.questions = {
+      [nativeDatasetQuestion.id()]: nativeDatasetQuestion,
+    };
+    metadata.tables[nestedNativeDatasetTable.id] = nestedNativeDatasetTable;
+
+    const table = getNativeQueryTable(
+      nativeDatasetQuestion.query() as NativeQuery,
+    );
+
+    it("should return a nested card table using the given query's question", () => {
+      expect(table?.getPlainObject()).toEqual(
+        expect.objectContaining({
+          display_name: "Native Dataset Question",
+          id: "card__1",
+          name: "Native Dataset Question",
+        }),
+      );
+
+      expect(table?.fields.map(field => field.getPlainObject())).toEqual([
+        {
+          ...PRODUCT_ID_WITH_OVERRIDING_METADATA.getPlainObject(),
+          display_name: "~*~Products.ID~*~",
+        },
+      ]);
+    });
+  });
+
+  describe("native query associated with botha `collection` and a `database`", () => {
+    const nativeQuestionWithCollection = new Question(
+      merge(NATIVE_QUERY_CARD, {
+        dataset_query: {
+          native: {
+            collection: PRODUCTS.name,
+          },
+        },
+      }),
+      metadata,
+    );
+
+    const table = getNativeQueryTable(
+      nativeQuestionWithCollection.query() as NativeQuery,
+    );
+
+    it("should return the concrete `table` associated with the given collection name", () => {
+      expect(table).toBe(PRODUCTS);
+    });
+  });
+
+  describe("basic native query question", () => {
+    const nativeQuestion = new Question(NATIVE_QUERY_CARD, metadata);
+    const table = getNativeQueryTable(nativeQuestion.query() as NativeQuery);
+
+    it("should not return a table", () => {
+      expect(table).toBeNull();
+    });
+  });
+});
diff --git a/frontend/src/metabase-lib/lib/queries/utils/nested-card-query-table.ts b/frontend/src/metabase-lib/lib/queries/utils/nested-card-query-table.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ed99a354551743f4ec976c93fbf2fbc74915985f
--- /dev/null
+++ b/frontend/src/metabase-lib/lib/queries/utils/nested-card-query-table.ts
@@ -0,0 +1,70 @@
+import {
+  getQuestionIdFromVirtualTableId,
+  getQuestionVirtualTableId,
+} from "metabase/lib/saved-questions";
+import type Table from "metabase-lib/lib/metadata/Table";
+import type Question from "metabase-lib/lib/Question";
+import type StructuredQuery from "../StructuredQuery";
+import type NativeQuery from "../NativeQuery";
+import { createVirtualField, createVirtualTable } from "./virtual-table";
+
+// This function expects a `sourceTableId` to exist in the `metadata.table` cache
+// It also expects the card associated with the `sourceTableId` to exist in the `metadata.question` cache
+export function getNestedCardTable(query: StructuredQuery): Table | null {
+  const sourceTableId = query.sourceTableId();
+  const metadata = query.metadata();
+  const nestedCardTable = metadata.table(sourceTableId);
+  if (nestedCardTable) {
+    return nestedCardTable;
+  }
+
+  const questionId = getQuestionIdFromVirtualTableId(sourceTableId);
+  const nestedQuestion = metadata.question(questionId);
+  // There are scenarios (and possible race conditions) in the application where
+  // the nested card table might not be available, but if we have access to a Question
+  // with result_metadata then we might as well use it to create virtual fields
+  if (nestedQuestion) {
+    return createVirtualTableUsingQuestionMetadata(nestedQuestion);
+  }
+
+  return null;
+}
+
+// Treat the Dataset/Model like a Question that uses itself as its source table
+// Expects the Question to have been fetched as a virtual table
+export function getDatasetTable(
+  query: StructuredQuery | NativeQuery,
+): Table | null {
+  const question = query.question();
+  const composedDatasetQuestion = question.composeDataset();
+  const composedQuestionQuery =
+    composedDatasetQuestion.query() as StructuredQuery;
+  return getNestedCardTable(composedQuestionQuery);
+}
+
+function createVirtualTableUsingQuestionMetadata(question: Question): Table {
+  const metadata = question.metadata();
+  const questionResultMetadata = question.getResultMetadata();
+  const questionDisplayName = question.displayName() as string;
+  const query = question.query() as StructuredQuery | NativeQuery;
+  const fields = questionResultMetadata.map((fieldMetadata: any) => {
+    const field = metadata.field(fieldMetadata.id);
+    const virtualField = field
+      ? field.clone(fieldMetadata)
+      : createVirtualField(fieldMetadata);
+
+    virtualField.query = query;
+    virtualField.metadata = metadata;
+
+    return virtualField;
+  });
+
+  return createVirtualTable({
+    id: getQuestionVirtualTableId(question.card()),
+    name: questionDisplayName,
+    display_name: questionDisplayName,
+    db: question?.database(),
+    fields,
+    metadata,
+  });
+}
diff --git a/frontend/src/metabase-lib/lib/queries/utils/structured-query-table.ts b/frontend/src/metabase-lib/lib/queries/utils/structured-query-table.ts
new file mode 100644
index 0000000000000000000000000000000000000000..54c074b34a0578f6fdf213ec3aa7e40e084c7e64
--- /dev/null
+++ b/frontend/src/metabase-lib/lib/queries/utils/structured-query-table.ts
@@ -0,0 +1,76 @@
+import type Field from "metabase-lib/lib/metadata/Field";
+import type Table from "metabase-lib/lib/metadata/Table";
+import type { Field as FieldRef } from "metabase-types/types/Query";
+
+import { isVirtualCardId } from "metabase/lib/saved-questions";
+
+import type StructuredQuery from "../StructuredQuery";
+import { createVirtualTable, createVirtualField } from "./virtual-table";
+import { getDatasetTable, getNestedCardTable } from "./nested-card-query-table";
+
+export function getStructuredQueryTable(query: StructuredQuery): Table | null {
+  const sourceQuery = query.sourceQuery();
+  // 1. Query has a source query. Use the source query as a table.
+  if (sourceQuery) {
+    return getSourceQueryTable(query);
+  }
+
+  // 2. Query has a source table that is a nested card.
+  const sourceTableId = query.sourceTableId();
+  if (isVirtualCardId(sourceTableId)) {
+    return getNestedCardTable(query);
+  }
+
+  // 3. The query's question is a dataset.
+  const question = query.question();
+  const isDataset = question?.isDataset() ?? false;
+  if (isDataset) {
+    return getDatasetTable(query);
+  }
+
+  // 4. The query's table is a concrete table, assuming one exists in `metadata`.
+  // Faiure to find a table at this point indicates that there is a bug.
+  return query.metadata().table(sourceTableId);
+}
+
+function getFieldsForSourceQueryTable(
+  originalQuery: StructuredQuery,
+  sourceQuery: StructuredQuery,
+): Field[] {
+  const metadata = originalQuery.metadata();
+  return sourceQuery.columns().map(column => {
+    // Not sure why we build out `id` like this, but it's what the old code did
+    const id: FieldRef = [
+      "field",
+      column.name,
+      {
+        "base-type": column.base_type,
+      },
+    ];
+
+    const virtualField = createVirtualField({
+      ...column,
+      id,
+      query: originalQuery,
+      metadata,
+    });
+
+    return virtualField;
+  });
+}
+
+function getSourceQueryTable(query: StructuredQuery): Table {
+  const sourceQuery = query.sourceQuery() as StructuredQuery;
+  const fields = getFieldsForSourceQueryTable(query, sourceQuery);
+  const sourceTableId = sourceQuery.sourceTableId() as Table["id"];
+
+  return createVirtualTable({
+    id: sourceTableId,
+    db: sourceQuery.database(),
+    fields,
+    metadata: sourceQuery.metadata(),
+    // intentionally set these to "" so that we fallback to a title of "Previous results" in join steps
+    display_name: "",
+    name: "",
+  });
+}
diff --git a/frontend/src/metabase-lib/lib/queries/utils/structured-query-table.unit.spec.ts b/frontend/src/metabase-lib/lib/queries/utils/structured-query-table.unit.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f55909d5010616f02a9f9f40df45cf9019bb0fb8
--- /dev/null
+++ b/frontend/src/metabase-lib/lib/queries/utils/structured-query-table.unit.spec.ts
@@ -0,0 +1,219 @@
+import _ from "underscore";
+
+import {
+  metadata,
+  PRODUCTS,
+  ORDERS,
+  SAMPLE_DATABASE,
+} from "__support__/sample_database_fixture";
+import Question from "metabase-lib/lib/Question";
+import Table from "metabase-lib/lib/metadata/Table";
+import Field from "metabase-lib/lib/metadata/Field";
+
+import type StructuredQuery from "../StructuredQuery";
+import { getStructuredQueryTable } from "./structured-query-table";
+
+describe("metabase-lib/queries/utils/structured-query-table", () => {
+  describe("Card that relies on another card as its source table", () => {
+    const NESTED_CARD_QUESTION = new Question(
+      {
+        dataset_query: {
+          database: SAMPLE_DATABASE?.id,
+          query: {
+            "source-table": "card__1",
+          },
+          type: "query",
+        },
+        id: 2,
+        display: "table",
+        database_id: SAMPLE_DATABASE?.id,
+        table_id: PRODUCTS.id,
+        name: "Question based on another question",
+      },
+      metadata,
+    );
+
+    const BASE_QUESTION = new Question(
+      {
+        ...PRODUCTS.newQuestion().card(),
+        id: 1,
+        display: "table",
+        database_id: SAMPLE_DATABASE?.id,
+        table_id: PRODUCTS.id,
+        name: "Base question",
+      },
+      metadata,
+    );
+
+    const NESTED_CARD_TABLE = new Table({
+      id: "card__1",
+      display_name: BASE_QUESTION.displayName(),
+      name: BASE_QUESTION.displayName(),
+    });
+    NESTED_CARD_TABLE.fields = [
+      new Field({
+        name: "boolean",
+        display_name: "boolean",
+        base_type: "type/Boolean",
+        effective_type: "type/Boolean",
+        semantic_type: null,
+        field_ref: [
+          "field",
+          "boolean",
+          {
+            "base-type": "type/Boolean",
+          },
+        ],
+      }),
+      new Field({
+        base_type: "type/Text",
+        display_name: "Foo",
+        effective_type: "type/Text",
+        field_ref: ["expression", "Foo"],
+        id: ["field", "Foo", { "base-type": "type/Text" }],
+        name: "Foo",
+        semantic_type: null,
+        table_id: "card__1",
+      }),
+      new Field({
+        id: PRODUCTS.CATEGORY.id,
+        display_name: "~*~ Category ~*~",
+      }),
+    ];
+
+    metadata.questions = {
+      [NESTED_CARD_QUESTION.id()]: NESTED_CARD_QUESTION,
+      [BASE_QUESTION.id()]: BASE_QUESTION,
+    };
+
+    metadata.tables[NESTED_CARD_TABLE.id] = NESTED_CARD_TABLE;
+
+    const table = getStructuredQueryTable(
+      NESTED_CARD_QUESTION.query() as StructuredQuery,
+    );
+
+    it("should return a table", () => {
+      expect(table).toBeInstanceOf(Table);
+    });
+
+    it("should return a virtual table based on the nested card", () => {
+      expect(table?.getPlainObject()).toEqual(
+        NESTED_CARD_TABLE.getPlainObject(),
+      );
+      expect(table?.fields).toEqual(NESTED_CARD_TABLE.fields);
+    });
+  });
+
+  describe("Dataset/model card", () => {
+    const ORDERS_USER_ID_FIELD = metadata
+      .field(ORDERS.USER_ID.id)
+      ?.getPlainObject();
+    const OVERWRITTEN_USER_ID_FIELD_METADATA = {
+      ...ORDERS_USER_ID_FIELD,
+      display_name: "Foo",
+      description: "Bar",
+      fk_target_field_id: 1,
+      semantic_type: "type/Price",
+      settings: {
+        show_mini_bar: true,
+      },
+    };
+
+    const ORDERS_DATASET = ORDERS.question()
+      .setCard({ ...ORDERS.question().card(), id: 3 })
+      .setDataset(true)
+      .setDisplayName("Dataset Question");
+
+    const ORDERS_DATASET_TABLE = new Table({
+      id: "card__3",
+      display_name: ORDERS_DATASET.displayName(),
+      name: ORDERS_DATASET.displayName(),
+      fields: [new Field(OVERWRITTEN_USER_ID_FIELD_METADATA)],
+    });
+
+    metadata.questions = {
+      [ORDERS_DATASET.id()]: ORDERS_DATASET,
+    };
+
+    metadata.tables[ORDERS_DATASET_TABLE.id] = ORDERS_DATASET_TABLE;
+
+    const table = getStructuredQueryTable(ORDERS_DATASET.query());
+    it("should return a nested card table using the given query's question", () => {
+      expect(table?.getPlainObject()).toEqual(
+        expect.objectContaining({
+          display_name: "Dataset Question",
+          id: "card__3",
+          name: "Dataset Question",
+        }),
+      );
+
+      expect(table?.fields.map(field => field.getPlainObject())).toEqual([
+        OVERWRITTEN_USER_ID_FIELD_METADATA,
+      ]);
+    });
+  });
+
+  describe("Card that relies on a source query", () => {
+    const SOURCE_QUERY_QUESTION = new Question(
+      {
+        dataset_query: {
+          database: SAMPLE_DATABASE?.id,
+          query: { "source-query": { "source-table": PRODUCTS.id } },
+          type: "query",
+        },
+        id: 2,
+        display: "table",
+        database_id: SAMPLE_DATABASE?.id,
+        table_id: PRODUCTS.id,
+        name: "Question using a nested query",
+      },
+      metadata,
+    );
+
+    metadata.questions = {
+      [SOURCE_QUERY_QUESTION.id()]: SOURCE_QUERY_QUESTION,
+    };
+
+    const table = getStructuredQueryTable(
+      SOURCE_QUERY_QUESTION.query() as StructuredQuery,
+    );
+
+    it("should return a virtual table based on the nested query", () => {
+      expect(table?.getPlainObject()).toEqual({
+        id: 3,
+        display_name: "",
+        name: "",
+      });
+    });
+
+    it("should contain fields", () => {
+      const fields = _.sortBy(
+        (table?.fields as Field[]).map(field => field.getPlainObject()),
+        "name",
+      );
+      const nestedQueryProductFields = _.sortBy(
+        PRODUCTS.fields.map((field: Field) => {
+          const column = field.dimension().column();
+          return {
+            ...column,
+            id: ["field", column.name, { "base-type": column.base_type }],
+            source: "fields",
+          };
+        }),
+        "name",
+      );
+
+      expect(fields).toEqual(nestedQueryProductFields);
+    });
+  });
+
+  describe("Card that has a concrete source table", () => {
+    const table = getStructuredQueryTable(
+      ORDERS.newQuestion().query() as StructuredQuery,
+    );
+
+    it("should return the concrete table stored on the Metadata object", () => {
+      expect(table).toBe(ORDERS);
+    });
+  });
+});
diff --git a/frontend/src/metabase-lib/lib/queries/utils/virtual-table.ts b/frontend/src/metabase-lib/lib/queries/utils/virtual-table.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f7a022b3ca334b9dbf1ee086e7f7e8443def98a5
--- /dev/null
+++ b/frontend/src/metabase-lib/lib/queries/utils/virtual-table.ts
@@ -0,0 +1,66 @@
+import Table from "metabase-lib/lib/metadata/Table";
+import Field from "metabase-lib/lib/metadata/Field";
+import type Database from "metabase-lib/lib/metadata/Database";
+import type Metadata from "metabase-lib/lib/metadata/Metadata";
+import type StructuredQuery from "../StructuredQuery";
+import type NativeQuery from "../NativeQuery";
+
+type VirtualTableProps = {
+  metadata: Metadata;
+  fields?: Field[];
+  db?: Database | null;
+} & Partial<Table>;
+
+type VirtualFieldProps = {
+  metadata: Metadata;
+  query: StructuredQuery | NativeQuery;
+} & Partial<Field>;
+
+// For when you have no Table
+export function createVirtualTable({
+  metadata,
+  fields,
+  db,
+  ...rest
+}: VirtualTableProps): Table {
+  const table = new Table({
+    name: "",
+    display_name: "",
+    ...rest,
+  });
+
+  Object.assign(table, {
+    fields: fields || [],
+    db,
+    metadata,
+    segments: [],
+    metrics: [],
+  });
+
+  table.metadata = metadata;
+  table.db = db;
+  table.fields = fields || [];
+
+  table.fields.forEach(field => {
+    field.table = table;
+    field.table_id = table.id;
+  });
+
+  return table;
+}
+
+export function createVirtualField({
+  metadata,
+  query,
+  ...rest
+}: VirtualFieldProps): Field {
+  const field = new Field({
+    source: "fields",
+    ...rest,
+  });
+
+  field.metadata = metadata;
+  field.query = query;
+
+  return field;
+}
diff --git a/frontend/src/metabase-lib/lib/queries/utils/virtual-table.unit.spec.ts b/frontend/src/metabase-lib/lib/queries/utils/virtual-table.unit.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..83acce6d5cd1507629ec6444e12800e9a12ee700
--- /dev/null
+++ b/frontend/src/metabase-lib/lib/queries/utils/virtual-table.unit.spec.ts
@@ -0,0 +1,68 @@
+import { createVirtualField, createVirtualTable } from "./virtual-table";
+import { metadata, PRODUCTS } from "__support__/sample_database_fixture";
+import Field from "metabase-lib/lib/metadata/Field";
+import Table from "metabase-lib/lib/metadata/Table";
+
+describe("metabase-lib/queries/utils/virtual-table", () => {
+  const query = PRODUCTS.newQuestion().query();
+  const field = createVirtualField({
+    id: 123,
+    metadata,
+    query,
+  });
+
+  describe("createVirtualField", () => {
+    it("should return a new Field instance", () => {
+      expect(field.id).toBe(123);
+      expect(field).toBeInstanceOf(Field);
+    });
+
+    it("should set `metadata` and `query` on the field instance but not its underlying plain object", () => {
+      expect(field.metadata).toBe(metadata);
+      expect(field.query).toBe(query);
+
+      const plainObject = field.getPlainObject() as any;
+      expect(plainObject.metadata).toBeUndefined();
+      expect(plainObject.query).toBeUndefined();
+    });
+  });
+
+  describe("createVirtualTable", () => {
+    const query = PRODUCTS.newQuestion().query();
+    const field1 = createVirtualField({
+      id: 1,
+      metadata,
+      query,
+    });
+    const field2 = createVirtualField({
+      id: 2,
+      metadata,
+      query,
+    });
+
+    const table = createVirtualTable({
+      id: 456,
+      metadata,
+      fields: [field1, field2],
+    });
+
+    it("should return a new Table instance", () => {
+      expect(table.id).toBe(456);
+      expect(table).toBeInstanceOf(Table);
+    });
+
+    it("should set `metadata` on the table instance but not its underlying plain object", () => {
+      expect(table.metadata).toBe(metadata);
+
+      const plainObject = table.getPlainObject() as any;
+      expect(plainObject.metadata).toBeUndefined();
+    });
+
+    it("should add a table reference to its fields", () => {
+      expect(table.fields.every(field => field.table === table)).toBe(true);
+      expect(table.fields.every(field => field.table_id === table.id)).toBe(
+        true,
+      );
+    });
+  });
+});
diff --git a/frontend/src/metabase-types/api/field.ts b/frontend/src/metabase-types/api/field.ts
index 702f885a382cb0f7cee9f7962f7d276bcb9f2d3b..150904493b47ef51b4d806d156103d1e91c29eb9 100644
--- a/frontend/src/metabase-types/api/field.ts
+++ b/frontend/src/metabase-types/api/field.ts
@@ -42,7 +42,7 @@ export interface Field {
   description: string | null;
   nfc_path: string[] | null;
 
-  fingerprint: FieldFingerprint;
+  fingerprint?: FieldFingerprint;
 }
 
 export type FieldDimension = {
diff --git a/frontend/src/metabase-types/api/mocks/field.ts b/frontend/src/metabase-types/api/mocks/field.ts
new file mode 100644
index 0000000000000000000000000000000000000000..de75b99377e515b4990a4edd7334d00bec87f421
--- /dev/null
+++ b/frontend/src/metabase-types/api/mocks/field.ts
@@ -0,0 +1,12 @@
+import { Field } from "metabase-types/api";
+
+export const createMockField = (opts?: Partial<Field>): Field => ({
+  id: 1,
+  display_name: "Mock Field",
+  table_id: 1,
+  name: "mock_field",
+  base_type: "type/Text",
+  description: null,
+  nfc_path: null,
+  ...opts,
+});
diff --git a/frontend/src/metabase/parameters/utils/mapping-options.unit.spec.js b/frontend/src/metabase/parameters/utils/mapping-options.unit.spec.js
index 1b840ebebff10a406af2113b93700df7e28071c8..e9300852aa31609e9d99867dafa1b5bb9900204c 100644
--- a/frontend/src/metabase/parameters/utils/mapping-options.unit.spec.js
+++ b/frontend/src/metabase/parameters/utils/mapping-options.unit.spec.js
@@ -98,13 +98,14 @@ describe("parameters/utils/mapping-options", () => {
           { type: "date/single" },
           structured({
             "source-query": {
-              "source-table": ORDERS.id,
+              "source-table": PRODUCTS.id,
             },
           }),
         );
         expect(options).toEqual([
           {
-            sectionName: null,
+            // this is a source query, and tables for source queries do not have a display_name
+            sectionName: "",
             name: "Created At",
             icon: "calendar",
             target: [
diff --git a/frontend/src/metabase/query_builder/actions/core/updateQuestion.ts b/frontend/src/metabase/query_builder/actions/core/updateQuestion.ts
index cc4c25ac1e7ae2b6b60797dddedd248258f093aa..81b54e16e79eda13ae76d550996bf537cfe9825d 100644
--- a/frontend/src/metabase/query_builder/actions/core/updateQuestion.ts
+++ b/frontend/src/metabase/query_builder/actions/core/updateQuestion.ts
@@ -227,8 +227,16 @@ export const updateQuestion = (
       }
     }
 
-    const currentDependencies = currentQuestion?.query().dependentMetadata();
-    const nextDependencies = newQuestion.query().dependentMetadata();
+    const currentDependencies = currentQuestion
+      ? [
+          ...currentQuestion.dependentMetadata(),
+          ...currentQuestion.query().dependentMetadata(),
+        ]
+      : [];
+    const nextDependencies = [
+      ...newQuestion.dependentMetadata(),
+      ...newQuestion.query().dependentMetadata(),
+    ];
     try {
       if (!_.isEqual(currentDependencies, nextDependencies)) {
         await dispatch(loadMetadataForCard(newQuestion.card()));
diff --git a/frontend/src/metabase/query_builder/actions/core/updateQuestion.unit.spec.ts b/frontend/src/metabase/query_builder/actions/core/updateQuestion.unit.spec.ts
index 84b234d248db7fbc624af2b5617a487104ecf9dd..a5a5c47a94aa5ae55cb7bb5e452b35911bb4733f 100644
--- a/frontend/src/metabase/query_builder/actions/core/updateQuestion.unit.spec.ts
+++ b/frontend/src/metabase/query_builder/actions/core/updateQuestion.unit.spec.ts
@@ -31,6 +31,7 @@ import {
   PEOPLE,
   PRODUCTS,
   state as entitiesState,
+  metadata,
 } from "__support__/sample_database_fixture";
 
 import * as navigation from "../navigation";
@@ -69,6 +70,12 @@ async function setup({
   shouldUpdateUrl,
   shouldStartAdHocQuestion,
 }: SetupOpts) {
+  if (originalQuestion.id()) {
+    metadata.questions = {
+      [originalQuestion.id()]: originalQuestion,
+    };
+  }
+
   const dispatch = jest.fn().mockReturnValue({ mock: "mock" });
 
   const queryResult = createMockDataset({
@@ -289,7 +296,6 @@ describe("QB Actions > updateQuestion", () => {
         describe(questionType, () => {
           it("turns question into ad-hoc", async () => {
             const { result } = await setup({ question });
-
             expect(result.card.id).toBeUndefined();
             expect(result.card.name).toBeUndefined();
             expect(result.card.description).toBeUndefined();
@@ -440,18 +446,63 @@ describe("QB Actions > updateQuestion", () => {
   });
 
   describe("structured", () => {
-    STRUCTURED_TEST_CASES.forEach(testCase => {
+    const modelTestCases = STRUCTURED_TEST_CASES.filter(testCase => {
+      return testCase.question.isDataset();
+    });
+    const structuredQuestionTestCases = STRUCTURED_TEST_CASES.filter(
+      testCase => {
+        return !testCase.question.isDataset();
+      },
+    );
+
+    modelTestCases.forEach(testCase => {
       const { question, questionType } = testCase;
 
       describe(questionType, () => {
-        it("doesn't refresh question metadata if dependent metadata doesn't change", async () => {
+        it("loads metadata for the model", async () => {
           const loadMetadataSpy = jest.spyOn(
             metadataActions,
             "loadMetadataForCard",
           );
 
           await setup({ question });
+          expect(loadMetadataSpy).toHaveBeenCalledTimes(1);
+        });
 
+        it("refreshes question metadata if there's difference in dependent metadata", async () => {
+          const loadMetadataSpy = jest.spyOn(
+            metadataActions,
+            "loadMetadataForCard",
+          );
+          const join = new Join(PRODUCTS_JOIN_CLAUSE);
+          const query = question.query() as StructuredQuery;
+          const questionWithJoin = query.join(join).question();
+
+          await setup({
+            question: questionWithJoin,
+            originalQuestion: question,
+          });
+
+          expect(loadMetadataSpy).toHaveBeenCalledWith(
+            expect.objectContaining({
+              dataset_query: questionWithJoin.datasetQuery(),
+            }),
+          );
+        });
+      });
+    });
+
+    structuredQuestionTestCases.forEach(testCase => {
+      const { question, questionType } = testCase;
+
+      describe(questionType, () => {
+        it("doesn't refresh question metadata if dependent metadata doesn't change", async () => {
+          const loadMetadataSpy = jest.spyOn(
+            metadataActions,
+            "loadMetadataForCard",
+          );
+
+          await setup({ question });
           expect(loadMetadataSpy).not.toHaveBeenCalled();
         });
 
diff --git a/frontend/src/metabase/query_builder/actions/models.js b/frontend/src/metabase/query_builder/actions/models.js
index c737d654f23acc920474961011e3d8e084972779..4ac8475c64125d12aff2f933e9bc9f7908716fe7 100644
--- a/frontend/src/metabase/query_builder/actions/models.js
+++ b/frontend/src/metabase/query_builder/actions/models.js
@@ -4,14 +4,16 @@ import { merge } from "icepick";
 import { t } from "ttag";
 
 import { isLocalField, isSameField } from "metabase/lib/query/field_ref";
-
 import { addUndo } from "metabase/redux/undo";
+import { loadMetadataForQueries } from "metabase/redux/metadata";
+import Questions from "metabase/entities/questions";
 
 import { getOriginalCard, getQuestion, getResultsMetadata } from "../selectors";
 
-import { apiUpdateQuestion, updateQuestion } from "./core";
+import { apiUpdateQuestion, updateQuestion, API_UPDATE_QUESTION } from "./core";
 import { runQuestionQuery } from "./querying";
 import { setQueryBuilderMode } from "./ui";
+import { getMetadata } from "metabase/selectors/metadata";
 
 export const setDatasetEditorTab = datasetEditorTab => dispatch => {
   dispatch(setQueryBuilderMode("dataset", { datasetEditorTab }));
@@ -28,8 +30,22 @@ export const onCancelDatasetChanges = () => (dispatch, getState) => {
 
 export const turnQuestionIntoDataset = () => async (dispatch, getState) => {
   const question = getQuestion(getState());
-  const dataset = question.setDataset(true);
-  await dispatch(apiUpdateQuestion(dataset, { rerunQuery: true }));
+
+  await dispatch(
+    Questions.actions.update(
+      {
+        id: question.id(),
+      },
+      question.setDataset(true).setDisplay("table").card(),
+    ),
+  );
+
+  const metadata = getMetadata(getState());
+  const dataset = metadata.question(question.id());
+
+  await dispatch(loadMetadataForQueries([], [dataset.dependentMetadata()]));
+
+  dispatch.action(API_UPDATE_QUESTION, dataset.card());
 
   dispatch(
     addUndo({
diff --git a/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetEditor.jsx b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetEditor.jsx
index 32f26e92cd6dd0976e4c7283dcab844750d99aae..16a9fca60af896e15c46e4caf19eb99164366881 100644
--- a/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetEditor.jsx
+++ b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetEditor.jsx
@@ -255,7 +255,7 @@ function DatasetEditor(props) {
   const focusedField = useMemo(() => {
     const field = fields[focusedFieldIndex];
     if (field) {
-      const fieldMetadata = metadata.field(field.id);
+      const fieldMetadata = metadata.field(field.id, field.table_id);
       return {
         ...fieldMetadata,
         ...field,
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterList/BulkFilterList.tsx b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterList/BulkFilterList.tsx
index 5f25bae6735e01a2d63dce2e7a0d460c43ed1537..2098b69f960e134f6000040b900346085f4fe12d 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterList/BulkFilterList.tsx
+++ b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterList/BulkFilterList.tsx
@@ -173,7 +173,7 @@ const SegmentListItem = ({
       <InlineOperatorSelector
         fieldName={t`Filter down to a segment`}
         iconName="filter"
-        tableName={isSearch ? query.table().displayName() : undefined}
+        tableName={isSearch ? query.table()?.displayName() : undefined}
       />
       <SegmentFilterSelect
         query={query}
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.tsx b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.tsx
index 1fe8550363dea6613aedf55b0bd9d3461a3619a8..b904e34ad616a05a0a7439bab7bf9007fae45589 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.tsx
+++ b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.tsx
@@ -252,7 +252,7 @@ const getQuery = (question: Question) => {
 const getTitle = (query: StructuredQuery, singleTable: boolean) => {
   const table = query.table();
 
-  if (singleTable) {
+  if (singleTable && table) {
     return t`Filter ${pluralize(table.displayName())} by`;
   } else {
     return t`Filter by`;
diff --git a/frontend/src/metabase/schema.js b/frontend/src/metabase/schema.js
index 894a44964316734728141a0ddaabcc03752ff27e..c828736972446072049aa07a0e94d55ae3154432 100644
--- a/frontend/src/metabase/schema.js
+++ b/frontend/src/metabase/schema.js
@@ -3,6 +3,7 @@
 import { schema } from "normalizr";
 import { generateSchemaId, entityTypeForObject } from "metabase/lib/schema";
 import { SAVED_QUESTIONS_VIRTUAL_DB_ID } from "metabase/lib/saved-questions";
+import { getUniqueFieldId } from "metabase-lib/lib/metadata/utils";
 
 export const QuestionSchema = new schema.Entity("questions");
 export const BookmarkSchema = new schema.Entity("bookmarks");
@@ -43,7 +44,20 @@ export const TableSchema = new schema.Entity(
     },
   },
 );
-export const FieldSchema = new schema.Entity("fields");
+
+export const FieldSchema = new schema.Entity("fields", undefined, {
+  processStrategy(field) {
+    const uniqueId = getUniqueFieldId(field.id, field.table_id);
+    return {
+      ...field,
+      uniqueId,
+    };
+  },
+  idAttribute: field => {
+    return getUniqueFieldId(field.id, field.table_id);
+  },
+});
+
 export const SegmentSchema = new schema.Entity("segments");
 export const MetricSchema = new schema.Entity("metrics");
 export const PersistedModelSchema = new schema.Entity("persistedModels");
diff --git a/frontend/src/metabase/selectors/metadata.js b/frontend/src/metabase/selectors/metadata.js
index a1bf385038c899779a778c05be03db537f22d346..6f792a721610dcb95031c5df88563c191968e45f 100644
--- a/frontend/src/metabase/selectors/metadata.js
+++ b/frontend/src/metabase/selectors/metadata.js
@@ -68,18 +68,41 @@ export const getShallowFields = getNormalizedFields;
 export const getShallowMetrics = getNormalizedMetrics;
 export const getShallowSegments = getNormalizedSegments;
 
-export const instantiateDatabase = obj => new Database(obj);
-export const instantiateSchema = obj => new Schema(obj);
-export const instantiateTable = obj => new Table(obj);
+export const instantiateDatabase = (obj, metadata) => {
+  const instance = new Database(obj);
+  instance.metadata = metadata;
+  return instance;
+};
+export const instantiateSchema = (obj, metadata) => {
+  const instance = new Schema(obj);
+  instance.metadata = metadata;
+  return instance;
+};
+export const instantiateTable = (obj, metadata) => {
+  const instance = new Table(obj);
+  instance.metadata = metadata;
+  return instance;
+};
 // We need a way to distinguish field objects that come from the server
 // vs. those that are created client-side to handle lossy transformations between
 // Field instances and FieldDimension instances.
 // There are scenarios where we are failing to convert FieldDimensions back into Fields,
 // and as a safeguard we instantiate a new Field that is missing most of its properties.
-export const instantiateField = obj =>
-  new Field({ ...obj, _comesFromEndpoint: true });
-export const instantiateSegment = obj => new Segment(obj);
-export const instantiateMetric = obj => new Metric(obj);
+export const instantiateField = (obj, metadata) => {
+  const instance = new Field({ ...obj, _comesFromEndpoint: true });
+  instance.metadata = metadata;
+  return instance;
+};
+export const instantiateSegment = (obj, metadata) => {
+  const instance = new Segment(obj);
+  instance.metadata = metadata;
+  return instance;
+};
+export const instantiateMetric = (obj, metadata) => {
+  const instance = new Metric(obj);
+  instance.metadata = metadata;
+  return instance;
+};
 export const instantiateQuestion = (obj, metadata) =>
   new Question(obj, metadata);
 
@@ -100,7 +123,7 @@ export const getMetadata = createSelector(
     meta.databases = copyObjects(meta, databases, instantiateDatabase);
     meta.schemas = copyObjects(meta, schemas, instantiateSchema);
     meta.tables = copyObjects(meta, tables, instantiateTable);
-    meta.fields = copyObjects(meta, fields, instantiateField);
+    meta.fields = copyObjects(meta, fields, instantiateField, "uniqueId");
     meta.segments = copyObjects(meta, segments, instantiateSegment);
     meta.metrics = copyObjects(meta, metrics, instantiateMetric);
     meta.questions = copyObjects(meta, questions, instantiateQuestion);
@@ -197,14 +220,18 @@ export const getSegments = createSelector(
 // UTILS:
 
 // clone each object in the provided mapping of objects
-export function copyObjects(metadata, objects, instantiate) {
+export function copyObjects(
+  metadata,
+  objects,
+  instantiate,
+  identifierProp = "id",
+) {
   const copies = {};
   for (const object of Object.values(objects)) {
-    if (object && object.id != null) {
-      copies[object.id] = instantiate(object, metadata);
-      copies[object.id].metadata = metadata;
+    if (object?.[identifierProp] != null) {
+      copies[object[identifierProp]] = instantiate(object, metadata);
     } else {
-      console.warn("Missing id:", object);
+      console.warn(`Missing ${identifierProp}:`, object);
     }
   }
   return copies;
diff --git a/frontend/test/__support__/sample_database_fixture.js b/frontend/test/__support__/sample_database_fixture.js
index c0e1db05cf518e7a52cb835536bc5ac2e1ec689d..5e5c25105399e741e4cf699e1dbce16761488aba 100644
--- a/frontend/test/__support__/sample_database_fixture.js
+++ b/frontend/test/__support__/sample_database_fixture.js
@@ -2,9 +2,11 @@
 import React from "react";
 import { Provider } from "react-redux";
 import { getStore } from "metabase/store";
+import { normalize } from "normalizr";
+import { chain } from "icepick";
 
 import { getMetadata } from "metabase/selectors/metadata";
-import { chain } from "icepick";
+import { FieldSchema } from "metabase/schema";
 
 import state from "./sample_database_fixture.json";
 export { default as state } from "./sample_database_fixture.json";
@@ -34,8 +36,16 @@ function aliasTablesAndFields(metadata) {
   }
 }
 
+function normalizeFields(fields) {
+  return normalize(fields, [FieldSchema]).entities.fields || {};
+}
+
 export function createMetadata(updateState = state => state) {
-  const stateModified = updateState(chain(state)).value();
+  const stateModified = updateState(chain(state)).thaw().value();
+  stateModified.entities.fields = normalizeFields(
+    stateModified.entities.fields,
+  );
+
   const metadata = getMetadata(stateModified);
   aliasTablesAndFields(metadata);
   return metadata;
@@ -106,6 +116,9 @@ export function makeMetadata(metadata) {
       }
     }
   }
+
+  metadata.fields = normalizeFields(metadata.fields);
+
   return getMetadata({ entities: metadata });
 }
 
diff --git a/frontend/test/metabase-lib/lib/Dimension.unit.spec.js b/frontend/test/metabase-lib/lib/Dimension.unit.spec.js
index 1e23c0f625d6afb2d3af250511caa8f728c176b8..9e3f4f27c916e4082d41a5b01be3a5c6d026f147 100644
--- a/frontend/test/metabase-lib/lib/Dimension.unit.spec.js
+++ b/frontend/test/metabase-lib/lib/Dimension.unit.spec.js
@@ -58,56 +58,8 @@ const nestedQuestionCard = {
   display: "table",
 };
 
-const cardWithResultMetadata = {
-  id: 123,
-  dataset: true,
-  display: "table",
-  visualization_settings: {},
-  dataset_query: {
-    type: "query",
-    database: SAMPLE_DATABASE.id,
-    query: {
-      "source-table": ORDERS.id,
-    },
-  },
-  result_metadata: [
-    {
-      id: ORDERS.ID.id,
-      display_name: "Foo",
-    },
-    {
-      name: ORDERS.TOTAL.name,
-      display_name: "Bar",
-    },
-  ],
-};
-
 const PRODUCT_CATEGORY_FIELD_ID = 21;
 
-const ORDERS_USER_ID_FIELD = metadata.field(ORDERS.USER_ID.id).getPlainObject();
-
-const OVERWRITTEN_USER_ID_FIELD_METADATA = {
-  ...ORDERS_USER_ID_FIELD,
-  display_name: "Foo",
-  description: "Bar",
-  fk_target_field_id: 1,
-  semantic_type: "type/Price",
-  settings: {
-    show_mini_bar: true,
-  },
-};
-
-const ORDERS_DATASET = ORDERS.question()
-  .setDataset(true)
-  .setResultsMetadata({
-    columns: [OVERWRITTEN_USER_ID_FIELD_METADATA],
-  });
-ORDERS_DATASET.card().id = 111;
-
-// It isn't actually possible to overwrite metadata for non-models,
-// it's just needed to test it's only possible for models
-const ORDERS_WITH_OVERWRITTEN_METADATA = ORDERS_DATASET.setDataset(false);
-
 describe("Dimension", () => {
   describe("STATIC METHODS", () => {
     describe("parseMBQL(mbql metadata)", () => {
@@ -373,32 +325,6 @@ describe("Dimension", () => {
           expect(field.id).toEqual(ORDERS.TOTAL.id);
           expect(field.base_type).toEqual("type/Float");
         });
-
-        it("should merge model's field results metadata with field info", () => {
-          const dimension = Dimension.parseMBQL(
-            ["field", ORDERS.USER_ID.id, null],
-            metadata,
-            ORDERS_DATASET.query(),
-          );
-
-          const field = dimension.field();
-          const fieldInfo = _.omit(field.getPlainObject(), "metadata", "query");
-
-          expect(fieldInfo).toEqual(OVERWRITTEN_USER_ID_FIELD_METADATA);
-        });
-
-        it("should not merge regular question's field results metadata with field info", () => {
-          const dimension = Dimension.parseMBQL(
-            ["field", ORDERS.USER_ID.id, null],
-            metadata,
-            ORDERS_WITH_OVERWRITTEN_METADATA.query(),
-          );
-
-          const field = dimension.field();
-          const fieldInfo = _.omit(field.getPlainObject(), "metadata", "query");
-
-          expect(fieldInfo).toEqual(ORDERS_USER_ID_FIELD);
-        });
       });
     });
   });
@@ -981,57 +907,6 @@ describe("Dimension", () => {
     });
   });
 
-  describe("Dimension connected to saved question with result_metadata", () => {
-    describe("field", () => {
-      it("should return a Field with properties from the field in the question's result_metadata", () => {
-        const questionWithResultMetadata = new Question(
-          cardWithResultMetadata,
-          metadata,
-        );
-        const fieldDimensionUsingIdProp = Dimension.parseMBQL(
-          ["field", ORDERS.ID.id, null],
-          metadata,
-          questionWithResultMetadata.query(),
-        );
-
-        const idField = fieldDimensionUsingIdProp.field();
-        expect(idField.id).toBe(ORDERS.ID.id);
-        expect(idField.display_name).toBe("Foo");
-        expect(idField.description).toBe(ORDERS.ID.description);
-      });
-    });
-  });
-
-  describe("Dimension connected to query based on nested card with result_metadata", () => {
-    describe("field", () => {
-      it("should return a Field with properties from the field in the question's result_metadata", () => {
-        metadata.questions[cardWithResultMetadata.id] = new Question(
-          cardWithResultMetadata,
-          metadata,
-        );
-
-        const questionWithResultMetadata = new Question(
-          cardWithResultMetadata,
-          metadata,
-        );
-        const unsavedQuestionBasedOnCard = questionWithResultMetadata
-          .composeThisQuery()
-          .setResultsMetadata([]);
-
-        const fieldDimensionUsingIdProp = Dimension.parseMBQL(
-          ["field", ORDERS.ID.id, null],
-          metadata,
-          unsavedQuestionBasedOnCard.query(),
-        );
-
-        const idField = fieldDimensionUsingIdProp.field();
-        expect(idField.id).toBe(ORDERS.ID.id);
-        expect(idField.display_name).toBe("Foo");
-        expect(idField.description).toBe(ORDERS.ID.description);
-      });
-    });
-  });
-
   describe("TemplateTagDimension", () => {
     describe("dimension tag (ie a field filter)", () => {
       const templateTagClause = ["template-tag", "foo"];
diff --git a/frontend/test/metabase-lib/lib/Question.unit.spec.js b/frontend/test/metabase-lib/lib/Question.unit.spec.js
index c0b8350343c4e50c8d1513e3ae06e6073fefe873..1e364fd1c3ae77bb562f4019f9503b1fdfebf5b0 100644
--- a/frontend/test/metabase-lib/lib/Question.unit.spec.js
+++ b/frontend/test/metabase-lib/lib/Question.unit.spec.js
@@ -939,7 +939,6 @@ describe("Question", () => {
       const question = new Question(
         {
           ...card,
-          dataset: true,
           result_metadata: [
             { semantic_type: SEMANTIC_TYPE.FK, fk_target_field_id: 5 },
           ],
@@ -954,7 +953,6 @@ describe("Question", () => {
       const question = new Question(
         {
           ...card,
-          dataset: true,
           result_metadata: [{ fk_target_field_id: 5 }],
         },
         metadata,
@@ -962,20 +960,6 @@ describe("Question", () => {
 
       expect(question.dependentMetadata()).toEqual([]);
     });
-
-    it("should return nothing for regular questions", () => {
-      const question = new Question(
-        {
-          ...card,
-          result_metadata: [
-            { semantic_type: SEMANTIC_TYPE.FK, fk_target_field_id: 5 },
-          ],
-        },
-        metadata,
-      );
-
-      expect(question.dependentMetadata()).toEqual([]);
-    });
   });
 
   describe("Question.prototype.setDashboardProps", () => {
diff --git a/frontend/test/metabase/lib/expressions/__support__/expressions.js b/frontend/test/metabase/lib/expressions/__support__/expressions.js
index 9ea1f436ddf3753b718363a67226be66247f953e..60b6b8c1f79f92260fdb5081152772174413cdf1 100644
--- a/frontend/test/metabase/lib/expressions/__support__/expressions.js
+++ b/frontend/test/metabase/lib/expressions/__support__/expressions.js
@@ -30,19 +30,66 @@ const metadata = makeMetadata({
     },
   },
   fields: {
-    1: { table: 1, display_name: "A", base_type: TYPE.Float },
-    2: { table: 1, display_name: "B", base_type: TYPE.Float },
-    3: { table: 1, display_name: "C", base_type: TYPE.Float },
-    10: { table: 1, display_name: "Toucan Sam", base_type: TYPE.Float },
-    11: { table: 1, display_name: "Sum", base_type: TYPE.Float },
-    12: { table: 1, display_name: "count", base_type: TYPE.Float },
-    13: { table: 1, display_name: "text", base_type: TYPE.Text },
-    14: { table: 1, display_name: "date", base_type: TYPE.DateTime },
+    1: {
+      id: 1,
+      table_id: 1,
+      table: 1,
+      display_name: "A",
+      base_type: TYPE.Float,
+    },
+    2: {
+      id: 2,
+      table_id: 1,
+      table: 1,
+      display_name: "B",
+      base_type: TYPE.Float,
+    },
+    3: {
+      id: 3,
+      table_id: 1,
+      table: 1,
+      display_name: "C",
+      base_type: TYPE.Float,
+    },
+    10: {
+      id: 10,
+      table_id: 1,
+      table: 1,
+      display_name: "Toucan Sam",
+      base_type: TYPE.Float,
+    },
+    11: {
+      id: 11,
+      table_id: 1,
+      table: 1,
+      display_name: "Sum",
+      base_type: TYPE.Float,
+    },
+    12: {
+      id: 12,
+      table_id: 1,
+      table: 1,
+      display_name: "count",
+      base_type: TYPE.Float,
+    },
+    13: {
+      id: 13,
+      table_id: 1,
+      table: 1,
+      display_name: "text",
+      base_type: TYPE.Text,
+    },
+    14: {
+      id: 14,
+      table_id: 1,
+      table: 1,
+      display_name: "date",
+      base_type: TYPE.DateTime,
+    },
   },
 });
 
 export const query = metadata.table(1).query();
-
 export const expressionOpts = { query, startRule: "expression" };
 export const aggregationOpts = { query, startRule: "aggregation" };
 export const filterOpts = { query, startRule: "boolean" };
diff --git a/frontend/test/metabase/scenarios/joins/joins.cy.spec.js b/frontend/test/metabase/scenarios/joins/joins.cy.spec.js
index 0b83af2630bb7196357ac9698cae2b20427139ce..e1b4a5557b5746360eadd6c71676b9600743e8bf 100644
--- a/frontend/test/metabase/scenarios/joins/joins.cy.spec.js
+++ b/frontend/test/metabase/scenarios/joins/joins.cy.spec.js
@@ -188,7 +188,7 @@ describe("scenarios > question > joined questions", () => {
     cy.icon("add_data").click();
 
     enterCustomColumnDetails({
-      formula: "[Question 5 → Sum of Rating] / [Sum of Rating]",
+      formula: "[Question 5 → Sum of Rating] / [Sum of Total]",
       name: "Sum Divide",
     });
 
diff --git a/frontend/test/metabase/scenarios/models/helpers/e2e-models-helpers.js b/frontend/test/metabase/scenarios/models/helpers/e2e-models-helpers.js
index 2a4d68155484b8ccfe4c654ec2ef01ec2dc7eab6..60191042e9dfe050d2611ff2b52343fa8756e2db 100644
--- a/frontend/test/metabase/scenarios/models/helpers/e2e-models-helpers.js
+++ b/frontend/test/metabase/scenarios/models/helpers/e2e-models-helpers.js
@@ -84,9 +84,9 @@ export function assertIsQuestion() {
 
 export function turnIntoModel() {
   interceptIfNotPreviouslyDefined({
-    method: "POST",
-    url: "/api/dataset",
-    alias: "dataset",
+    method: "PUT",
+    url: "/api/card/*",
+    alias: "cardUpdate",
   });
 
   openQuestionActions();
@@ -96,7 +96,7 @@ export function turnIntoModel() {
   modal().within(() => {
     cy.button("Turn this into a model").click();
   });
-  cy.wait("@dataset");
+  cy.wait("@cardUpdate");
 }
 
 export function selectFromDropdown(option, clickOpts) {
diff --git a/frontend/test/metabase/scenarios/models/models-with-aggregation-and-breakout.cy.spec.js b/frontend/test/metabase/scenarios/models/models-with-aggregation-and-breakout.cy.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..daba298a815c80626266e66bc01e4720c6d54856
--- /dev/null
+++ b/frontend/test/metabase/scenarios/models/models-with-aggregation-and-breakout.cy.spec.js
@@ -0,0 +1,37 @@
+import { restore } from "__support__/e2e/helpers";
+
+import { turnIntoModel } from "./helpers/e2e-models-helpers";
+import { SAMPLE_DATABASE } from "__support__/e2e/cypress_sample_database";
+const { ORDERS, ORDERS_ID } = SAMPLE_DATABASE;
+
+describe("scenarios > models with aggregation and breakout", () => {
+  beforeEach(() => {
+    restore();
+    cy.signInAsAdmin();
+    cy.intercept("POST", "/api/dataset").as("dataset");
+    cy.intercept("PUT", "/api/card/*").as("updateCard");
+
+    cy.createQuestion(
+      {
+        name: "model with aggregation & breakout",
+        display: "line",
+        query: {
+          "source-table": ORDERS_ID,
+          aggregation: [["distinct", ["field", ORDERS.PRODUCT_ID, null]]],
+          breakout: [
+            ["field", ORDERS.CREATED_AT, { "temporal-unit": "month" }],
+          ],
+        },
+      },
+      { visitQuestion: true },
+    );
+  });
+
+  it("should be possible to convert a question with an aggregation and breakout into a model", () => {
+    turnIntoModel();
+    cy.wait("@updateCard");
+
+    cy.findByText("Created At: Month");
+    cy.findByText("Distinct values of Product ID");
+  });
+});
diff --git a/frontend/test/metabase/scenarios/models/reproductions/23449-remapped-custom-value.cy.spec.js b/frontend/test/metabase/scenarios/models/reproductions/23449-remapped-custom-value.cy.spec.js
index 794c6404bbdce3ff67f539d4ba106933a2a10f50..410ca85db15b4819254837b4f73a9e3c0dc5a624 100644
--- a/frontend/test/metabase/scenarios/models/reproductions/23449-remapped-custom-value.cy.spec.js
+++ b/frontend/test/metabase/scenarios/models/reproductions/23449-remapped-custom-value.cy.spec.js
@@ -5,7 +5,7 @@ const { REVIEWS, REVIEWS_ID } = SAMPLE_DATABASE;
 
 const questionDetails = { query: { "source-table": REVIEWS_ID, limit: 2 } };
 
-describe.skip("issue 23449", () => {
+describe("issue 23449", () => {
   beforeEach(() => {
     restore();
     cy.signInAsAdmin();
@@ -36,13 +36,13 @@ describe.skip("issue 23449", () => {
 });
 
 function turnIntoModel() {
-  cy.intercept("POST", "/api/dataset").as("dataset");
+  cy.intercept("PUT", "/api/card/*").as("cardUpdate");
 
   openQuestionActions();
   cy.findByText("Turn into a model").click();
   cy.findByText("Turn this into a model").click();
 
-  cy.wait("@dataset").then(({ response }) => {
+  cy.wait("@cardUpdate").then(({ response }) => {
     expect(response.body.error).to.not.exist;
   });
 }