diff --git a/.clj-kondo/hooks/clojure/core.clj b/.clj-kondo/hooks/clojure/core.clj
index beea24347e9ca098b41b999f822809ddb9b6b133..c9cd4c404cfd0a5a3dfa515f4df24c5e1ef655f5 100644
--- a/.clj-kondo/hooks/clojure/core.clj
+++ b/.clj-kondo/hooks/clojure/core.clj
@@ -21,7 +21,9 @@
      toucan2.core/delete!
      toucan2.core/update!
      toucan2.core/insert!
+     toucan2.core/insert-returning-instance!
      toucan2.core/insert-returning-instances!
+     toucan2.core/insert-returning-pk!
      toucan2.core/insert-returning-pks!
      clojure.core.async/<!!
      clojure.core.async/>!!
diff --git a/e2e/snapshot-creators/default.cy.snap.js b/e2e/snapshot-creators/default.cy.snap.js
index 34d4e72f4f9069042369e11b6f968d2a78021354..33b5a9743f7e46fa7e70a6c2acfe787435985475 100644
--- a/e2e/snapshot-creators/default.cy.snap.js
+++ b/e2e/snapshot-creators/default.cy.snap.js
@@ -214,7 +214,7 @@ describe("snapshots", () => {
     cy.createQuestion({
       name: "Orders Model",
       query: { "source-table": ORDERS_ID },
-      dataset: true,
+      type: "model",
     });
   }
 
diff --git a/e2e/support/commands/api/question.js b/e2e/support/commands/api/question.js
index f9aa5609c40dd284c3b6055164d2943b06dd5401..fc84604e1153293738191adf00906090680e3f8d 100644
--- a/e2e/support/commands/api/question.js
+++ b/e2e/support/commands/api/question.js
@@ -30,12 +30,12 @@ Cypress.Commands.add(
 
 /**
  *
- * @param {("query"|"native")} type
+ * @param {("query"|"native")} queryType
  *
  * @param {object} questionDetails
  * @param {string} [questionDetails.name="test question"]
  * @param {string} questionDetails.description
- * @param {boolean} questionDetails.dataset - Is this a Model or no? (model = dataset)
+ * @param {("question"|"model")} questionDetails.type Entity type
  * @param {object} questionDetails.native
  * @param {object} questionDetails.query
  * @param {number} [questionDetails.database=1]
@@ -52,11 +52,11 @@ Cypress.Commands.add(
  * @param {string} customOptions.interceptAlias - We need distinctive endpoint aliases for cases where we have multiple questions or nested questions.
  */
 function question(
-  type,
+  queryType,
   {
     name = "test question",
     description,
-    dataset = false,
+    type = "question",
     native,
     query,
     database = SAMPLE_DB_ID,
@@ -81,8 +81,8 @@ function question(
       name,
       description,
       dataset_query: {
-        type,
-        [type]: type === "native" ? native : query,
+        type: queryType,
+        [queryType]: queryType === "native" ? native : query,
         database,
       },
       display,
@@ -104,27 +104,27 @@ function question(
         cy.wrap(body.id).as(idAlias);
       }
 
-      if (dataset || enable_embedding) {
+      if (type === "model" || enable_embedding) {
         cy.request("PUT", `/api/card/${body.id}`, {
-          dataset,
+          type,
           enable_embedding,
           embedding_params,
         });
       }
 
       if (loadMetadata || visitQuestion) {
-        dataset
-          ? cy.intercept("POST", `/api/dataset`).as("dataset")
-          : // We need to use the wildcard because endpoint for pivot tables has the following format: `/api/card/pivot/${id}/query`
-            cy
-              .intercept("POST", `/api/card/**/${body.id}/query`)
-              .as(interceptAlias);
-
-        const url = dataset ? `/model/${body.id}` : `/question/${body.id}`;
-        cy.visit(url);
-
-        // Wait for `result_metadata` to load
-        dataset ? cy.wait("@dataset") : cy.wait("@" + interceptAlias);
+        if (type === "model") {
+          cy.intercept("POST", `/api/dataset`).as("dataset");
+          cy.visit(`/model/${body.id}`);
+          cy.wait("@dataset"); // Wait for `result_metadata` to load
+        } else {
+          // We need to use the wildcard because endpoint for pivot tables has the following format: `/api/card/pivot/${id}/query`
+          cy.intercept("POST", `/api/card/**/${body.id}/query`).as(
+            interceptAlias,
+          );
+          cy.visit(`/question/${body.id}`);
+          cy.wait("@" + interceptAlias); // Wait for `result_metadata` to load
+        }
       }
     });
 }
diff --git a/e2e/support/helpers/e2e-qa-databases-helpers.js b/e2e/support/helpers/e2e-qa-databases-helpers.js
index b08d848c6ff8b91664ad6022d6bf2bef45454b8e..cf428a413c73e15e66e402b7dd6903b7c7fd9f0d 100644
--- a/e2e/support/helpers/e2e-qa-databases-helpers.js
+++ b/e2e/support/helpers/e2e-qa-databases-helpers.js
@@ -220,7 +220,7 @@ export const createModelFromTableName = ({
         query: {
           "source-table": tableId,
         },
-        dataset: true,
+        type: "model",
       },
       {
         wrapId: true,
diff --git a/e2e/support/integration/visit-dashboard.js b/e2e/support/integration/visit-dashboard.js
index 84f32731f1882ca01148f92bbe2f76e8a3dcc422..e73c136fb2ed3ce00297e84a2eb3b92d34641cb5 100644
--- a/e2e/support/integration/visit-dashboard.js
+++ b/e2e/support/integration/visit-dashboard.js
@@ -34,7 +34,7 @@ const questionDetails = {
 const modelDetails = {
   name: "GUI Model",
   query: { "source-table": PRODUCTS_ID },
-  dataset: true,
+  type: "model",
 };
 
 const pivotTable = {
diff --git a/e2e/test/scenarios/actions/reproductions/32974-linked-parameter-via-url-does-not-work.cy.spec.js b/e2e/test/scenarios/actions/reproductions/32974-linked-parameter-via-url-does-not-work.cy.spec.js
index b9254709f886dcede480902d0c3ed1bce95bc5b6..50487c2de287dc670df78d2c15d8335638051a68 100644
--- a/e2e/test/scenarios/actions/reproductions/32974-linked-parameter-via-url-does-not-work.cy.spec.js
+++ b/e2e/test/scenarios/actions/reproductions/32974-linked-parameter-via-url-does-not-work.cy.spec.js
@@ -45,7 +45,7 @@ const MODEL_DETAILS = {
   name: "Products model",
   query: { "source-table": PRODUCTS_ID },
   database: SAMPLE_DB_ID,
-  dataset: true,
+  type: "model",
 };
 
 const EXPECTED_UPDATED_VALUE = 999;
diff --git a/e2e/test/scenarios/admin/databases/default-sample-database.cy.spec.js b/e2e/test/scenarios/admin/databases/default-sample-database.cy.spec.js
index 65a2a904313a1728796bdf582f7a9b30f9f543a7..222b6cbc0c95fb9a1747f53d86aec47d746ae808 100644
--- a/e2e/test/scenarios/admin/databases/default-sample-database.cy.spec.js
+++ b/e2e/test/scenarios/admin/databases/default-sample-database.cy.spec.js
@@ -124,7 +124,7 @@ describe("scenarios > admin > databases > sample database", () => {
     );
     cy.intercept("DELETE", `/api/database/${SAMPLE_DB_ID}`).as("delete");
     // model
-    cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { dataset: true });
+    cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { type: "model" });
     // Create a segment through API
     createSegment({
       name: "Small orders",
diff --git a/e2e/test/scenarios/collections/collection-pinned-overview.cy.spec.js b/e2e/test/scenarios/collections/collection-pinned-overview.cy.spec.js
index a3e8dd82015b4a28306f58cc27c9364e9df569f6..1fb189fa82324be8c4bdbb746fdf6fe3496def7c 100644
--- a/e2e/test/scenarios/collections/collection-pinned-overview.cy.spec.js
+++ b/e2e/test/scenarios/collections/collection-pinned-overview.cy.spec.js
@@ -107,7 +107,7 @@ describe("scenarios > collection pinned items overview", () => {
   });
 
   it("should be able to pin a model", () => {
-    cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { dataset: true });
+    cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { type: "model" });
 
     openRootCollection();
     openUnpinnedItemMenu(MODEL_NAME);
diff --git a/e2e/test/scenarios/collections/collections.cy.spec.js b/e2e/test/scenarios/collections/collections.cy.spec.js
index f4f905a7c115f09ed3113a8a32bfbac758365b88..f868a17f184e8c3a6540e09cd98d7fedb5da1b00 100644
--- a/e2e/test/scenarios/collections/collections.cy.spec.js
+++ b/e2e/test/scenarios/collections/collections.cy.spec.js
@@ -583,7 +583,7 @@ describe("scenarios > collection defaults", () => {
     });
 
     it("should allow to x-ray models from collection views", () => {
-      cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { dataset: true });
+      cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { type: "model" });
       cy.visit("/collection/root");
 
       openEllipsisMenuFor("Orders");
diff --git a/e2e/test/scenarios/collections/permissions.cy.spec.js b/e2e/test/scenarios/collections/permissions.cy.spec.js
index acd796df0e30027e615148f3c4abe53f8b63e3e9..faf8086123e27c05a8cefb1e0672c4efe71b6dc1 100644
--- a/e2e/test/scenarios/collections/permissions.cy.spec.js
+++ b/e2e/test/scenarios/collections/permissions.cy.spec.js
@@ -170,7 +170,7 @@ describe("collection permissions", () => {
                   cy.skipOn(user === "nodata");
                   cy.createNativeQuestion({
                     name: "Model",
-                    dataset: true,
+                    type: "model",
                     native: {
                       query: "SELECT * FROM ORDERS",
                     },
diff --git a/e2e/test/scenarios/dashboard/dashboard-back-navigation.cy.spec.js b/e2e/test/scenarios/dashboard/dashboard-back-navigation.cy.spec.js
index e86b2ab28b97efb713a0af3c4c68be5a5955b9c4..4ca49af12535ca11081852ebb112428499177493 100644
--- a/e2e/test/scenarios/dashboard/dashboard-back-navigation.cy.spec.js
+++ b/e2e/test/scenarios/dashboard/dashboard-back-navigation.cy.spec.js
@@ -128,7 +128,7 @@ describe("scenarios > dashboard > dashboard back navigation", () => {
     { tags: "@slow" },
     () => {
       const cardTitle = "Orders by Subtotal";
-      cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { dataset: true });
+      cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { type: "model" });
       cy.visit(
         `/auto/dashboard/model/${ORDERS_QUESTION_ID}?#show=${MAX_CARDS}`,
       );
@@ -369,7 +369,7 @@ const createDashboardWithCards = () => {
   const modelDetails = {
     name: "Orders model",
     query: { "source-table": ORDERS_ID },
-    dataset: true,
+    type: "model",
   };
 
   const actionDetails = {
diff --git a/e2e/test/scenarios/embedding/embedding-smoketests.cy.spec.js b/e2e/test/scenarios/embedding/embedding-smoketests.cy.spec.js
index a4565d522eb4f771bd5e4cab9cc4714a7e500247..dd039426a8ce803179dc5accbd4dd55cde8a8f0f 100644
--- a/e2e/test/scenarios/embedding/embedding-smoketests.cy.spec.js
+++ b/e2e/test/scenarios/embedding/embedding-smoketests.cy.spec.js
@@ -29,7 +29,7 @@ describe("scenarios > embedding > smoke tests", { tags: "@OSS" }, () => {
   it("should not offer to share or embed models (metabase#20815)", () => {
     cy.intercept("POST", "/api/dataset").as("dataset");
 
-    cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { dataset: true });
+    cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { type: "model" });
 
     cy.visit(`/model/${ORDERS_QUESTION_ID}`);
     cy.wait("@dataset");
diff --git a/e2e/test/scenarios/joins/reproductions/22859-multi-nested-joins-wrong-aliasing.cy.spec.js b/e2e/test/scenarios/joins/reproductions/22859-multi-nested-joins-wrong-aliasing.cy.spec.js
index 252631009f08193d24fce5e6a8dc91dfe4de41b1..24203e0f5e0fff8a3c7b99fd855828bfe3df1611 100644
--- a/e2e/test/scenarios/joins/reproductions/22859-multi-nested-joins-wrong-aliasing.cy.spec.js
+++ b/e2e/test/scenarios/joins/reproductions/22859-multi-nested-joins-wrong-aliasing.cy.spec.js
@@ -75,7 +75,7 @@ describe("issue 22859 - multiple levels of nesting", () => {
   it("model based on multi-level nested saved question should work (metabase#22859-1)", () => {
     cy.get("@q2Id").then(id => {
       // Convert the second question to a model
-      cy.request("PUT", `/api/card/${id}`, { dataset: true });
+      cy.request("PUT", `/api/card/${id}`, { type: "model" });
 
       cy.intercept("POST", "/api/dataset").as("dataset");
       cy.visit(`/model/${id}`);
diff --git a/e2e/test/scenarios/models/model-indexes.cy.spec.js b/e2e/test/scenarios/models/model-indexes.cy.spec.js
index 65f78d6d8fd8cf022bdfef9960260c9343b1ab73..50ba792d3024a3fd8685e50a68a9405f96ce5db2 100644
--- a/e2e/test/scenarios/models/model-indexes.cy.spec.js
+++ b/e2e/test/scenarios/models/model-indexes.cy.spec.js
@@ -28,7 +28,7 @@ describe("scenarios > model indexes", () => {
       {
         name: "Products Model",
         query: { "source-table": PRODUCTS_ID },
-        dataset: true,
+        type: "model",
       },
       { wrapId: true, idAlias: "modelId" },
     );
@@ -161,7 +161,7 @@ describe("scenarios > model indexes", () => {
       {
         name: "People Model",
         query: { "source-table": PEOPLE_ID },
-        dataset: true,
+        type: "model",
       },
       {
         wrapId: true,
diff --git a/e2e/test/scenarios/models/models-metadata.cy.spec.js b/e2e/test/scenarios/models/models-metadata.cy.spec.js
index 788e2274efa0aff6c8f8eede627a9177d79570f0..c4a5cf320d6f806197bfef5e0812b75313f160e9 100644
--- a/e2e/test/scenarios/models/models-metadata.cy.spec.js
+++ b/e2e/test/scenarios/models/models-metadata.cy.spec.js
@@ -41,7 +41,7 @@ describe("scenarios > models metadata", () => {
           "source-table": ORDERS_ID,
           limit: 5,
         },
-        dataset: true,
+        type: "model",
       };
 
       cy.createQuestion(modelDetails).then(({ body: { id } }) => {
@@ -126,7 +126,7 @@ describe("scenarios > models metadata", () => {
     cy.createNativeQuestion(
       {
         name: "Native Model",
-        dataset: true,
+        type: "model",
         native: {
           query: "SELECT * FROM ORDERS LIMIT 5",
         },
@@ -176,7 +176,7 @@ describe("scenarios > models metadata", () => {
     cy.createNativeQuestion(
       {
         name: "Native Model",
-        dataset: true,
+        type: "model",
         native: {
           query: "SELECT * FROM ORDERS LIMIT 5",
         },
@@ -198,7 +198,7 @@ describe("scenarios > models metadata", () => {
     cy.createNativeQuestion(
       {
         name: "Native Model",
-        dataset: true,
+        type: "model",
         native: {
           query: "SELECT * FROM ORDERS LIMIT 5",
         },
@@ -234,7 +234,7 @@ describe("scenarios > models metadata", () => {
 
       cy.createNativeQuestion({
         name: "Native Model",
-        dataset: true,
+        type: "model",
         native: {
           query: "SELECT * FROM ORDERS LIMIT 5",
         },
@@ -289,7 +289,7 @@ describe("scenarios > models metadata", () => {
       cy.createNativeQuestion(
         {
           name: "Native Model",
-          dataset: true,
+          type: "model",
           native: {
             query: "select * from orders limit 100",
           },
@@ -427,7 +427,7 @@ describe("scenarios > models metadata", () => {
 
       const questionDetails = {
         name: "22521",
-        dataset: true,
+        type: "model",
         query: {
           "source-table": PRODUCTS_ID,
           limit: 5,
diff --git a/e2e/test/scenarios/models/models-query-editor.cy.spec.js b/e2e/test/scenarios/models/models-query-editor.cy.spec.js
index 07a7bd00b3d5228c6f5e4283eac7f62f9c02de16..f800b6c9ff275d5f690017f2854d08689f61a3c2 100644
--- a/e2e/test/scenarios/models/models-query-editor.cy.spec.js
+++ b/e2e/test/scenarios/models/models-query-editor.cy.spec.js
@@ -23,7 +23,7 @@ describe("scenarios > models query editor", () => {
     beforeEach(() => {
       cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, {
         name: "Orders Model",
-        dataset: true,
+        type: "model",
       });
     });
 
@@ -122,7 +122,7 @@ describe("scenarios > models query editor", () => {
       cy.createNativeQuestion(
         {
           name: "Native Model",
-          dataset: true,
+          type: "model",
           native: {
             query: "SELECT * FROM orders limit 5",
           },
@@ -161,7 +161,7 @@ describe("scenarios > models query editor", () => {
       cy.createNativeQuestion(
         {
           name: "Native Model",
-          dataset: true,
+          type: "model",
           native: {
             query: "SELECT * FROM orders limit 5",
           },
@@ -199,7 +199,7 @@ describe("scenarios > models query editor", () => {
       cy.createNativeQuestion(
         {
           name: "Erroring Model",
-          dataset: true,
+          type: "model",
           native: {
             // Let's use API to type the most of the query, but stil make it invalid
             query: "SELECT 1 FROM",
diff --git a/e2e/test/scenarios/models/models-revision-history.cy.spec.js b/e2e/test/scenarios/models/models-revision-history.cy.spec.js
index daebf109ba4b90430f84e4b640dbc48b844a0da1..ab9fc1aab7e6e8c2f346b8096c6423adaf2dc89f 100644
--- a/e2e/test/scenarios/models/models-revision-history.cy.spec.js
+++ b/e2e/test/scenarios/models/models-revision-history.cy.spec.js
@@ -7,7 +7,7 @@ describe("scenarios > models > revision history", () => {
     cy.signInAsAdmin();
     cy.request("PUT", `/api/card/${ORDERS_BY_YEAR_QUESTION_ID}`, {
       name: "Orders Model",
-      dataset: true,
+      type: "model",
     });
   });
 
diff --git a/e2e/test/scenarios/models/models.cy.spec.js b/e2e/test/scenarios/models/models.cy.spec.js
index 38e1a7ef285b6958972cef9d93b02e6029d1cbd1..436aa257e319bfbced631bed72038daec58a6f63 100644
--- a/e2e/test/scenarios/models/models.cy.spec.js
+++ b/e2e/test/scenarios/models/models.cy.spec.js
@@ -192,7 +192,7 @@ describe("scenarios > models", () => {
   });
 
   it("allows to turn a model back into a saved question", () => {
-    cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { dataset: true });
+    cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { type: "model" });
     cy.intercept("PUT", `/api/card/${ORDERS_QUESTION_ID}`).as("cardUpdate");
     cy.visit(`/model/${ORDERS_QUESTION_ID}`);
 
@@ -222,7 +222,7 @@ describe("scenarios > models", () => {
   });
 
   it("redirects to /model URL when opening a model with /question URL", () => {
-    cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { dataset: true });
+    cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { type: "model" });
     // Important - do not use visitQuestion(ORDERS_QUESTION_ID) here!
     cy.visit("/question/" + ORDERS_QUESTION_ID);
     cy.wait("@dataset");
@@ -234,7 +234,7 @@ describe("scenarios > models", () => {
   describe("data picker", () => {
     beforeEach(() => {
       cy.intercept("GET", "/api/search*").as("search");
-      cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { dataset: true });
+      cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { type: "model" });
     });
 
     it("transforms the data picker", () => {
@@ -348,7 +348,7 @@ describe("scenarios > models", () => {
     beforeEach(() => {
       cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, {
         name: "Orders Model",
-        dataset: true,
+        type: "model",
       });
     });
 
@@ -493,7 +493,7 @@ describe("scenarios > models", () => {
     cy.createNativeQuestion({
       native: { query: "SELECT * FROM products" },
     }).then(({ body: { id: modelId } }) => {
-      cy.request("PUT", `/api/card/${modelId}`, { dataset: true }).then(() => {
+      cy.request("PUT", `/api/card/${modelId}`, { type: "model" }).then(() => {
         cy.visit(`/model/${modelId}/query`);
         cy.get(".ace_editor:not(.ace_autocomplete)")
           .should("be.visible")
@@ -510,7 +510,7 @@ describe("scenarios > models", () => {
     cy.intercept("POST", "/api/card/*/query").as("cardQuery");
     cy.createNativeQuestion({
       name: "TEST MODEL",
-      dataset: true,
+      type: "model",
       native: {
         query: "select * from orders",
       },
@@ -555,7 +555,7 @@ describe("scenarios > models", () => {
         "source-table": ORDERS_ID,
         limit: 5,
       },
-      dataset: true,
+      type: "model",
     };
 
     beforeEach(() => {
diff --git a/e2e/test/scenarios/models/reproductions/19180-native-model-results-disappear.cy.spec.js b/e2e/test/scenarios/models/reproductions/19180-native-model-results-disappear.cy.spec.js
index 3fb0e8e509a3304182498ab058153930d8a1ca36..6c0f873175ea6512d84979071af57f3393e69dcb 100644
--- a/e2e/test/scenarios/models/reproductions/19180-native-model-results-disappear.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/19180-native-model-results-disappear.cy.spec.js
@@ -13,7 +13,7 @@ describe("issue 19180", () => {
 
   it("shouldn't drop native model query results after leaving the query editor", () => {
     cy.createNativeQuestion(QUESTION).then(({ body: { id: QUESTION_ID } }) => {
-      cy.request("PUT", `/api/card/${QUESTION_ID}`, { dataset: true }).then(
+      cy.request("PUT", `/api/card/${QUESTION_ID}`, { type: "model" }).then(
         () => {
           cy.visit(`/model/${QUESTION_ID}/query`);
           cy.wait("@cardQuery");
diff --git a/e2e/test/scenarios/models/reproductions/20042-nodata-user-blank-screen.cy.spec.js b/e2e/test/scenarios/models/reproductions/20042-nodata-user-blank-screen.cy.spec.js
index 7cb793bd81e2914da3eb8c8e35d9c658ca9e75e2..8c45c760b733b3e9fdea5b76d4c3e2e2e8775b41 100644
--- a/e2e/test/scenarios/models/reproductions/20042-nodata-user-blank-screen.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/20042-nodata-user-blank-screen.cy.spec.js
@@ -10,7 +10,7 @@ describe("issue 20042", () => {
 
     cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, {
       name: "Orders Model",
-      dataset: true,
+      type: "model",
     });
 
     cy.signIn("nodata");
diff --git a/e2e/test/scenarios/models/reproductions/20045-rerun-model-adds-hash.cy.spec.js b/e2e/test/scenarios/models/reproductions/20045-rerun-model-adds-hash.cy.spec.js
index 154ad5d66b125515dca0c9e27cbda2e32bef8840..495a2a93ff3f2110c10bd586721f8d40412374b3 100644
--- a/e2e/test/scenarios/models/reproductions/20045-rerun-model-adds-hash.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/20045-rerun-model-adds-hash.cy.spec.js
@@ -10,7 +10,7 @@ describe("issue 20045", () => {
 
     cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, {
       name: "Orders Model",
-      dataset: true,
+      type: "model",
     });
   });
 
diff --git a/e2e/test/scenarios/models/reproductions/20517-edit-metadata-empty-description.cy.spec.js b/e2e/test/scenarios/models/reproductions/20517-edit-metadata-empty-description.cy.spec.js
index 3f4cf01bf79683dfae093ade7cb5eb977592d160..506eb7f03663c15df5437cbf0eb98a408d0679f2 100644
--- a/e2e/test/scenarios/models/reproductions/20517-edit-metadata-empty-description.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/20517-edit-metadata-empty-description.cy.spec.js
@@ -9,7 +9,7 @@ const modelDetails = {
     "source-table": ORDERS_ID,
     limit: 5,
   },
-  dataset: true,
+  type: "model",
 };
 
 describe("issue 20517", () => {
diff --git a/e2e/test/scenarios/models/reproductions/20624-model-metadata-should-override-column-settings.cy.spec.js b/e2e/test/scenarios/models/reproductions/20624-model-metadata-should-override-column-settings.cy.spec.js
index 0f0c39fe5700910461c101515010a6f7bba392bf..d866641ab99abc1b955d665954ca03ec45b95e1c 100644
--- a/e2e/test/scenarios/models/reproductions/20624-model-metadata-should-override-column-settings.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/20624-model-metadata-should-override-column-settings.cy.spec.js
@@ -5,7 +5,7 @@ const renamedColumn = "TITLE renamed";
 
 const questionDetails = {
   name: "20624",
-  dataset: true,
+  type: "model",
   native: { query: "select * from PRODUCTS limit 2" },
   visualization_settings: {
     column_settings: { '["name","TITLE"]': { column_title: renamedColumn } },
diff --git a/e2e/test/scenarios/models/reproductions/22517-add-remove-column-drops-metadata.cy.spec.js b/e2e/test/scenarios/models/reproductions/22517-add-remove-column-drops-metadata.cy.spec.js
index aa1704b711c2a04c7d251a7787b1fccd12f5b3e3..b9be4dc903af596b4fb774d1156ea1982bf95a35 100644
--- a/e2e/test/scenarios/models/reproductions/22517-add-remove-column-drops-metadata.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/22517-add-remove-column-drops-metadata.cy.spec.js
@@ -12,7 +12,7 @@ describe("issue 22517", () => {
       {
         name: "22517",
         native: { query: `select * from orders` },
-        dataset: true,
+        type: "model",
       },
       { visitQuestion: true },
     );
diff --git a/e2e/test/scenarios/models/reproductions/22518.cy.spec.js b/e2e/test/scenarios/models/reproductions/22518.cy.spec.js
index a2cb50788fa04b18ad87aeb5f42dcf81e42789dc..f2dcbb7317ac61cb22ab1e6b558c7a28b4de748a 100644
--- a/e2e/test/scenarios/models/reproductions/22518.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/22518.cy.spec.js
@@ -15,7 +15,7 @@ describe("issue 22518", () => {
         native: {
           query: "select 1 id, 'a' foo",
         },
-        dataset: true,
+        type: "model",
       },
       { visitQuestion: true },
     );
diff --git a/e2e/test/scenarios/models/reproductions/22715-remapped-values-override-column-identifier.cy.spec.js b/e2e/test/scenarios/models/reproductions/22715-remapped-values-override-column-identifier.cy.spec.js
index 8cb99b976232730ecd8178aca03421ba25c03c87..126822e28f7463d3efd06899339d56b49d47bff4 100644
--- a/e2e/test/scenarios/models/reproductions/22715-remapped-values-override-column-identifier.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/22715-remapped-values-override-column-identifier.cy.spec.js
@@ -23,7 +23,7 @@ describe("filtering based on the remapped column name should result in a correct
       visitQuestion(id);
 
       // Turn the question into a model
-      cy.request("PUT", `/api/card/${id}`, { dataset: true });
+      cy.request("PUT", `/api/card/${id}`, { type: "model" });
 
       // Let's go straight to the model metadata editor
       cy.visit(`/model/${id}/metadata`);
diff --git a/e2e/test/scenarios/models/reproductions/23024-cannot-apply-dash-filter-native-model.cy.spec.js b/e2e/test/scenarios/models/reproductions/23024-cannot-apply-dash-filter-native-model.cy.spec.js
index d2e9bcda2cd6b16e46cca2f06da0d89c81627e61..1f5d4402f6ab218ece36b555ce850af45b93e294 100644
--- a/e2e/test/scenarios/models/reproductions/23024-cannot-apply-dash-filter-native-model.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/23024-cannot-apply-dash-filter-native-model.cy.spec.js
@@ -25,7 +25,7 @@ describe("issue 23024", () => {
           query: `select *
                   from products limit 5`,
         },
-        dataset: true,
+        type: "model",
       },
       { wrapId: true, idAlias: "modelId" },
     );
diff --git a/e2e/test/scenarios/models/reproductions/23421-visualization-settins-breaks-ui.cy.spec.js b/e2e/test/scenarios/models/reproductions/23421-visualization-settins-breaks-ui.cy.spec.js
index bbabf815789fcf5825be6534990ab895d38fc929..7a0b96383115e7016563ff279a51c7c14487faf0 100644
--- a/e2e/test/scenarios/models/reproductions/23421-visualization-settins-breaks-ui.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/23421-visualization-settins-breaks-ui.cy.spec.js
@@ -12,7 +12,7 @@ const questionDetails = {
     "table.pivot_column": "orphaned1",
     "table.cell_column": "orphaned2",
   },
-  dataset: true,
+  type: "model",
 };
 
 describe("issue 23421", () => {
diff --git a/e2e/test/scenarios/models/reproductions/25537-model-picker-locale.cy.spec.js b/e2e/test/scenarios/models/reproductions/25537-model-picker-locale.cy.spec.js
index 336bd94d88d0f14942757a5a8d0a309518910d82..f3285fa55db973b9c24fc384c4d92ed9374355db 100644
--- a/e2e/test/scenarios/models/reproductions/25537-model-picker-locale.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/25537-model-picker-locale.cy.spec.js
@@ -6,7 +6,7 @@ const { ORDERS_ID } = SAMPLE_DATABASE;
 const questionDetails = {
   name: "Orders model",
   query: { "source-table": ORDERS_ID },
-  dataset: true,
+  type: "model",
 };
 
 describe("issue 25537", () => {
diff --git a/e2e/test/scenarios/models/reproductions/26091-new-models-picker.cy.spec.js b/e2e/test/scenarios/models/reproductions/26091-new-models-picker.cy.spec.js
index d08653ecdd3b09880dec0e9cb62bb4600bca2575..50aaf75d7732d062b2d8885e1be4672308e69fca 100644
--- a/e2e/test/scenarios/models/reproductions/26091-new-models-picker.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/26091-new-models-picker.cy.spec.js
@@ -7,7 +7,7 @@ const { PRODUCTS_ID } = SAMPLE_DATABASE;
 const modelDetails = {
   name: "Old model",
   query: { "source-table": PRODUCTS_ID },
-  dataset: true,
+  type: "model",
 };
 
 describe("issue 26091", () => {
diff --git a/e2e/test/scenarios/models/reproductions/28193-cannot-use-custom-column.cy.spec.js b/e2e/test/scenarios/models/reproductions/28193-cannot-use-custom-column.cy.spec.js
index c5334e92f83e6c75212aaa88cb0609881b7b890c..4ab761b70916338707a4592e2a7c8033a4895110 100644
--- a/e2e/test/scenarios/models/reproductions/28193-cannot-use-custom-column.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/28193-cannot-use-custom-column.cy.spec.js
@@ -11,7 +11,7 @@ describe("issue 28193", () => {
     cy.signInAsAdmin();
 
     // Turn the question into a model
-    cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { dataset: true });
+    cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { type: "model" });
   });
 
   it("should be able to use custom column in a model query (metabase#28193)", () => {
diff --git a/e2e/test/scenarios/models/reproductions/29378-actions-search-crash.cy.spec.js b/e2e/test/scenarios/models/reproductions/29378-actions-search-crash.cy.spec.js
index 5b017ba712c4939fee71fee3bb44337273221f91..88400abb1ff928ba85a48d05bfa24037588b04a0 100644
--- a/e2e/test/scenarios/models/reproductions/29378-actions-search-crash.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/29378-actions-search-crash.cy.spec.js
@@ -33,7 +33,7 @@ describe("issue 29378", () => {
   });
 
   it("should not crash the model detail page after searching for an action (metabase#29378)", () => {
-    cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { dataset: true });
+    cy.request("PUT", `/api/card/${ORDERS_QUESTION_ID}`, { type: "model" });
     createAction(ACTION_DETAILS);
 
     cy.visit(`/model/${ORDERS_QUESTION_ID}/detail`);
diff --git a/e2e/test/scenarios/models/reproductions/29517-native-remapped-model-drill-through-click-behavior.cy.spec.js b/e2e/test/scenarios/models/reproductions/29517-native-remapped-model-drill-through-click-behavior.cy.spec.js
index 17986ea8dc08c41bdcbd9eefe9d66f4e5579653a..5099bf186d6e02d0f10e81216e8d296c230ef263 100644
--- a/e2e/test/scenarios/models/reproductions/29517-native-remapped-model-drill-through-click-behavior.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/29517-native-remapped-model-drill-through-click-behavior.cy.spec.js
@@ -10,7 +10,7 @@ import { ORDERS_DASHBOARD_ID } from "e2e/support/cypress_sample_instance_data";
 
 const questionDetails = {
   name: "29517",
-  dataset: true,
+  type: "model",
   native: {
     query:
       'Select Orders."ID" AS "ID",\nOrders."CREATED_AT" AS "CREATED_AT"\nFrom Orders',
diff --git a/e2e/test/scenarios/models/reproductions/29951-model-editor-results-metadata.cy.spec.js b/e2e/test/scenarios/models/reproductions/29951-model-editor-results-metadata.cy.spec.js
index 185065b7eaa5231d557f4af3dbb47593cb525ffe..c789411d71c12f09316b3cb10be2d79b27b2fbc2 100644
--- a/e2e/test/scenarios/models/reproductions/29951-model-editor-results-metadata.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/29951-model-editor-results-metadata.cy.spec.js
@@ -18,7 +18,7 @@ const questionDetails = {
     },
     limit: 2,
   },
-  dataset: true,
+  type: "model",
 };
 
 describe("issue 29951", { requestTimeout: 10000, viewportWidth: 1600 }, () => {
diff --git a/e2e/test/scenarios/models/reproductions/31309-duplicating-a-model-must-duplicate-question.cy.spec.js b/e2e/test/scenarios/models/reproductions/31309-duplicating-a-model-must-duplicate-question.cy.spec.js
index 8b3de914b331c902206dd52c47b1e3e8f541d872..400ddddc932c7b150c3f332148200856b92e95ab 100644
--- a/e2e/test/scenarios/models/reproductions/31309-duplicating-a-model-must-duplicate-question.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/31309-duplicating-a-model-must-duplicate-question.cy.spec.js
@@ -39,9 +39,8 @@ describe("issue 31309", () => {
       {
         name: "model",
         query: TEST_QUERY,
-        type: "query",
         database: SAMPLE_DB_ID,
-        dataset: true,
+        type: "model",
       },
       {
         visitQuestion: true,
diff --git a/e2e/test/scenarios/models/reproductions/31663-no-model-fks.cy.spec.js b/e2e/test/scenarios/models/reproductions/31663-no-model-fks.cy.spec.js
index 8ce0dc17832d850528033643e0110e97a1a26b47..0bd5fb2dda5cafa22b5e4983a584c8505f8c3140 100644
--- a/e2e/test/scenarios/models/reproductions/31663-no-model-fks.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/31663-no-model-fks.cy.spec.js
@@ -18,7 +18,7 @@ describe("issue 31663", () => {
     cy.createQuestion(
       {
         name: "Products Model",
-        dataset: true,
+        type: "model",
         query: { "source-table": PRODUCTS_ID },
       },
       { visitQuestion: true },
diff --git a/e2e/test/scenarios/models/reproductions/31905-duplicated-api-request.cy.spec.js b/e2e/test/scenarios/models/reproductions/31905-duplicated-api-request.cy.spec.js
index 907c307589524bc7db688eb17fe24fbc3f494788..351b32da9c9cbc3416b90505ca4c998f4532e3fb 100644
--- a/e2e/test/scenarios/models/reproductions/31905-duplicated-api-request.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/31905-duplicated-api-request.cy.spec.js
@@ -13,7 +13,7 @@ describe("issue 31905", () => {
     cy.createQuestion(
       {
         name: "Orders Model",
-        dataset: true,
+        type: "model",
         query: { "source-table": ORDERS_ID, limit: 2 },
       },
       { visitQuestion: true },
diff --git a/e2e/test/scenarios/models/reproductions/32963-model-question-display-lock.cy.spec.js b/e2e/test/scenarios/models/reproductions/32963-model-question-display-lock.cy.spec.js
index d20f04636d81f2d95bcfde32ca02d52379e1992c..35668fab3bbfc13dba9b5a598e2d6a3e3c232f49 100644
--- a/e2e/test/scenarios/models/reproductions/32963-model-question-display-lock.cy.spec.js
+++ b/e2e/test/scenarios/models/reproductions/32963-model-question-display-lock.cy.spec.js
@@ -18,7 +18,7 @@ describe("issue 32963", () => {
     cy.createQuestion(
       {
         name: "Orders Model",
-        dataset: true,
+        type: "model",
         query: { "source-table": ORDERS_ID },
       },
       { visitQuestion: true },
diff --git a/e2e/test/scenarios/native/data_ref.cy.spec.js b/e2e/test/scenarios/native/data_ref.cy.spec.js
index e0373c20ad07498dc41571ddf8d1eb8b6e3fb992..8ca4dcf0a13db6ab4f659586b01cbc8eb78a5fd8 100644
--- a/e2e/test/scenarios/native/data_ref.cy.spec.js
+++ b/e2e/test/scenarios/native/data_ref.cy.spec.js
@@ -46,7 +46,7 @@ describe("scenarios > native question > data reference sidebar", () => {
         name: "Native Products Model",
         description: "A model of the Products table",
         native: { query: "select id as renamed_id from products" },
-        dataset: true,
+        type: "model",
       },
       { visitQuestion: true },
     );
diff --git a/e2e/test/scenarios/native/native_subquery.cy.spec.js b/e2e/test/scenarios/native/native_subquery.cy.spec.js
index b4d722e35e21c8eee406cf41da297c46c830bd39..01aacbb04356887bce3b0a2ac5004be3516a9332 100644
--- a/e2e/test/scenarios/native/native_subquery.cy.spec.js
+++ b/e2e/test/scenarios/native/native_subquery.cy.spec.js
@@ -26,7 +26,7 @@ describe("scenarios > question > native subquery", () => {
         native: {
           query: "SELECT id AS another_unique_column_name FROM PEOPLE",
         },
-        dataset: true,
+        type: "model",
       }).then(({ body: { id: questionId2 } }) => {
         const tagName1 = `#${questionId1}-a-people-question`;
         const queryText = `{{${tagName1}}}`;
@@ -70,7 +70,7 @@ describe("scenarios > question > native subquery", () => {
         native: {
           query: "SELECT id FROM PEOPLE",
         },
-        dataset: true,
+        type: "model",
       }).then(({ body: { id: questionId2 } }) => {
         // Move question 2 to personal collection
         cy.visit(`/question/${questionId2}`);
diff --git a/e2e/test/scenarios/onboarding/metabot.cy.spec.js b/e2e/test/scenarios/onboarding/metabot.cy.spec.js
index d3005839e7a19b96bbffa65e7e8c39f01dc27ff5..938def5e3a8c4755fcb40872122cdcce6399c520 100644
--- a/e2e/test/scenarios/onboarding/metabot.cy.spec.js
+++ b/e2e/test/scenarios/onboarding/metabot.cy.spec.js
@@ -27,7 +27,7 @@ const MODEL_DETAILS = {
   query: {
     "source-table": PRODUCTS_ID,
   },
-  dataset: true,
+  type: "model",
 };
 
 const PROMPT_RESPONSE = {
diff --git a/e2e/test/scenarios/onboarding/search/reproductions/28788-search-results-overflow.cy.spec.js b/e2e/test/scenarios/onboarding/search/reproductions/28788-search-results-overflow.cy.spec.js
index b9cc3bca67c271ffae1f846119f233b19ff9a2e6..097ed9f202211ed1ca95dda114b9eb705fc4cc00 100644
--- a/e2e/test/scenarios/onboarding/search/reproductions/28788-search-results-overflow.cy.spec.js
+++ b/e2e/test/scenarios/onboarding/search/reproductions/28788-search-results-overflow.cy.spec.js
@@ -14,7 +14,7 @@ describe("issue 28788", () => {
   it("search results container should not be scrollable horizontally (metabase#28788)", () => {
     const questionDetails = {
       name: `28788-${LONG_STRING}`,
-      dataset: true,
+      type: "model",
       description: LONG_STRING,
       query: {
         "source-table": PEOPLE_ID,
diff --git a/e2e/test/scenarios/onboarding/search/search.cy.spec.js b/e2e/test/scenarios/onboarding/search/search.cy.spec.js
index a019815fcea09e217e327fabfc423a4c5b98b047..8925e176e9308b899bff39e4391b0a3e6e99d0d9 100644
--- a/e2e/test/scenarios/onboarding/search/search.cy.spec.js
+++ b/e2e/test/scenarios/onboarding/search/search.cy.spec.js
@@ -314,7 +314,7 @@ describe("scenarios > search", () => {
         cy.createQuestion({
           name: "Orders Model",
           query: { "source-table": ORDERS_ID },
-          dataset: true,
+          type: "model",
         }).then(({ body: { id } }) => {
           createAction({
             name: "Update orders quantity",
@@ -340,7 +340,7 @@ describe("scenarios > search", () => {
           {
             name: "Products Model",
             query: { "source-table": PRODUCTS_ID },
-            dataset: true,
+            type: "model",
           },
           { wrapId: true, idAlias: "modelId" },
         );
diff --git a/e2e/test/scenarios/organization/bookmarks-collection.cy.spec.js b/e2e/test/scenarios/organization/bookmarks-collection.cy.spec.js
index 864f39247d3adec3a1028cde768f976cc7f3ef66..0eaeca0d717fe2755116c78701ff704e7e78c61a 100644
--- a/e2e/test/scenarios/organization/bookmarks-collection.cy.spec.js
+++ b/e2e/test/scenarios/organization/bookmarks-collection.cy.spec.js
@@ -100,7 +100,7 @@ describe("scenarios > organization > bookmarks > collection", () => {
     cy.createQuestion({
       name: "Orders Model",
       query: { "source-table": STATIC_ORDERS_ID, aggregation: [["count"]] },
-      dataset: true,
+      type: "model",
     });
 
     addThenRemoveBookmarkTo("Orders Model");
diff --git a/e2e/test/scenarios/organization/content-verification.cy.spec.js b/e2e/test/scenarios/organization/content-verification.cy.spec.js
index 139d6b1f1867497313ae1d987ae4014d4845e1ff..4df687a264517c8883de8256c6b73886673f61c8 100644
--- a/e2e/test/scenarios/organization/content-verification.cy.spec.js
+++ b/e2e/test/scenarios/organization/content-verification.cy.spec.js
@@ -45,7 +45,7 @@ describeEE("scenarios > premium > content verification", () => {
 
       cy.log("Turn the question into a model and try again");
       cy.request("PUT", `/api/card/${ORDERS_COUNT_QUESTION_ID}`, {
-        dataset: true,
+        type: "model",
       });
 
       cy.intercept("POST", "/api/dataset").as("dataset");
diff --git a/e2e/test/scenarios/question/new.cy.spec.js b/e2e/test/scenarios/question/new.cy.spec.js
index fbe6efc6b1b2957851f6b07ff5dd49d11ad00913..c19802c90895cc8b2981e1cb2ef86bfdbc98bf95 100644
--- a/e2e/test/scenarios/question/new.cy.spec.js
+++ b/e2e/test/scenarios/question/new.cy.spec.js
@@ -424,7 +424,7 @@ describeOSS(
       cy.createQuestion({
         name: "Orders Model",
         query: { "source-table": ORDERS_ID },
-        dataset: true,
+        type: "model",
       });
 
       cy.visit("/question/new");
diff --git a/e2e/test/scenarios/question/notebook.cy.spec.js b/e2e/test/scenarios/question/notebook.cy.spec.js
index d04e602c1935a8c354b5b1ea4c58d20c3393b871..0e807372927ced7d1928ec7e039f6354c831d70a 100644
--- a/e2e/test/scenarios/question/notebook.cy.spec.js
+++ b/e2e/test/scenarios/question/notebook.cy.spec.js
@@ -465,7 +465,7 @@ describe("scenarios > question > notebook", { tags: "@slow" }, () => {
     cy.createQuestion({
       name: "Products model",
       query: { "source-table": PRODUCTS_ID },
-      dataset: true,
+      type: "model",
       display: "table",
     });
 
@@ -473,7 +473,7 @@ describe("scenarios > question > notebook", { tags: "@slow" }, () => {
       {
         name: "Orders model",
         query: { "source-table": ORDERS_ID },
-        dataset: true,
+        type: "model",
         display: "table",
       },
       { visitQuestion: true },
@@ -533,7 +533,7 @@ describe("scenarios > question > notebook", { tags: "@slow" }, () => {
     () => {
       cy.createNativeQuestion({
         name: "Orders, Model",
-        dataset: true,
+        type: "model",
         native: { query: "SELECT * FROM ORDERS" },
       });
 
diff --git a/e2e/test/scenarios/question/reproductions/34414-populate-field-values.cy.spec.js b/e2e/test/scenarios/question/reproductions/34414-populate-field-values.cy.spec.js
index 6bce944a12e9c8146dd5f9d7488c4b83362f6a6b..2036125a34f8813a05862c34dfbe5915eedce889 100644
--- a/e2e/test/scenarios/question/reproductions/34414-populate-field-values.cy.spec.js
+++ b/e2e/test/scenarios/question/reproductions/34414-populate-field-values.cy.spec.js
@@ -13,7 +13,7 @@ const { INVOICES_ID } = SAMPLE_DATABASE;
 const INVOICE_MODEL_DETAILS = {
   name: "Invoices Model",
   query: { "source-table": INVOICES_ID },
-  dataset: true,
+  type: "model",
 };
 
 describe("issue 34414", () => {
diff --git a/e2e/test/scenarios/sharing/public-sharing.cy.spec.js b/e2e/test/scenarios/sharing/public-sharing.cy.spec.js
index b406a7dde519d668138f416d598d3011e0ce9dd6..9e32c8dfbfb3964978fefc0f5a89314f5ffc814c 100644
--- a/e2e/test/scenarios/sharing/public-sharing.cy.spec.js
+++ b/e2e/test/scenarios/sharing/public-sharing.cy.spec.js
@@ -205,7 +205,7 @@ describe("scenarios > admin > settings > public sharing", () => {
       query: {
         "source-table": ORDERS_ID,
       },
-      dataset: true,
+      type: "model",
     }).then(({ body }) => {
       const modelId = body.id;
       cy.wrap(modelId).as("modelId");
diff --git a/frontend/src/metabase-lib/Question.ts b/frontend/src/metabase-lib/Question.ts
index bfbe2dbb88d694ba571ec8422196fdfe9b06e443..a80a54af623763e29b43726c3a3d61d6902c5e5e 100644
--- a/frontend/src/metabase-lib/Question.ts
+++ b/frontend/src/metabase-lib/Question.ts
@@ -24,6 +24,7 @@ import { sortObject } from "metabase-lib/utils";
 
 import type {
   Card as CardObject,
+  CardType,
   CollectionId,
   DatabaseId,
   DatasetQuery,
@@ -259,16 +260,30 @@ class Question {
 
   /**
    * returns whether this question is a model
-   * @returns boolean
+   * @deprecated Use Question.prototype.type instead
    */
-  isDataset() {
+  isDataset(): boolean {
     return this._card && this._card.dataset;
   }
 
-  setDataset(dataset) {
+  type(): CardType | undefined {
+    return this._card && this._card.type;
+  }
+
+  /**
+   * @deprecated Use Question.prototype.setType instead
+   */
+  private _setDataset(dataset: boolean) {
     return this.setCard(assoc(this.card(), "dataset", dataset));
   }
 
+  setType(type: CardType) {
+    const dataset = type === "model";
+    // _setDataset is still called for backwards compatibility
+    // as we're migrating "dataset" -> "type" incrementally
+    return this.setCard(assoc(this.card(), "type", type))._setDataset(dataset);
+  }
+
   isPersisted() {
     return this._card && this._card.persisted;
   }
@@ -436,10 +451,6 @@ class Question {
     return this.setSettings({ ...this.settings(), ...settings });
   }
 
-  type(): string {
-    return this.datasetQuery().type;
-  }
-
   creationType(): string {
     return this.card().creationType;
   }
diff --git a/frontend/src/metabase-lib/queries/structured/Join.ts b/frontend/src/metabase-lib/queries/structured/Join.ts
index 737df55be7fb5b0eeb4363c5fd4b02b44a404188..38d776afbbfe06be684a34cece87b30f539c3751 100644
--- a/frontend/src/metabase-lib/queries/structured/Join.ts
+++ b/frontend/src/metabase-lib/queries/structured/Join.ts
@@ -100,7 +100,7 @@ class Join extends MBQLObjectClause {
       ? new StructuredQuery(
           this.legacyQuery({ useStructuredQuery: true })
             .question()
-            .setDataset(false),
+            .setType("question"),
           {
             type: "query",
             query: {
@@ -112,7 +112,7 @@ class Join extends MBQLObjectClause {
       ? new StructuredQuery(
           this.legacyQuery({ useStructuredQuery: true })
             .question()
-            .setDataset(false),
+            .setType("question"),
           {
             type: "query",
             query: sourceQuery,
diff --git a/frontend/src/metabase-types/api/card.ts b/frontend/src/metabase-types/api/card.ts
index de7d302121cb8b40e9d472a199e8db97d6960169..8dafa3052a5ecaf67054a650bd7cf5788fed091d 100644
--- a/frontend/src/metabase-types/api/card.ts
+++ b/frontend/src/metabase-types/api/card.ts
@@ -7,12 +7,18 @@ import type { UserInfo } from "./user";
 import type { Collection } from "./collection";
 import type { SmartScalarComparison } from "./visualization-settings";
 
+export type CardType = "model" | "question";
+
 export interface Card<Q extends DatasetQuery = DatasetQuery>
   extends UnsavedCard<Q> {
   id: CardId;
   name: string;
   description: string | null;
+  /**
+   * @deprecated Use "type" instead
+   */
   dataset: boolean;
+  type: CardType;
   public_uuid: string | null;
 
   /* Indicates whether static embedding for this card has been published */
diff --git a/frontend/src/metabase-types/api/mocks/card.ts b/frontend/src/metabase-types/api/mocks/card.ts
index 1518a5ca29055fca3b0b38d094783a607050fe67..60edf49f58fb592a12826afa783826c710dd1d6c 100644
--- a/frontend/src/metabase-types/api/mocks/card.ts
+++ b/frontend/src/metabase-types/api/mocks/card.ts
@@ -25,6 +25,7 @@ export const createMockCard = (opts?: Partial<Card>): Card => ({
   visualization_settings: createMockVisualizationSettings(),
   result_metadata: [],
   dataset: false,
+  type: "question",
   can_write: true,
   cache_ttl: null,
   collection: null,
diff --git a/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/QueryActionContextProvider.tsx b/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/QueryActionContextProvider.tsx
index d5761cf228d486afcd4121a17ffbd69027123c11..6ff0d604cacc2c207fda61cd22bc0c4be85ae021 100644
--- a/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/QueryActionContextProvider.tsx
+++ b/frontend/src/metabase/actions/containers/ActionCreator/ActionContext/QueryActionContextProvider/QueryActionContextProvider.tsx
@@ -67,6 +67,7 @@ function convertActionToQuestionCard(
       action.visualization_settings as VisualizationSettings,
 
     dataset: false,
+    type: "question",
     can_write: true,
     public_uuid: null,
     collection_id: null,
diff --git a/frontend/src/metabase/entities/questions.js b/frontend/src/metabase/entities/questions.js
index 01eb1b394e87f93d01f2fd1474c2ab834bccab11..2f7040401329d14257255854f0dfeecd86cab725 100644
--- a/frontend/src/metabase/entities/questions.js
+++ b/frontend/src/metabase/entities/questions.js
@@ -128,6 +128,7 @@ const Questions = createEntity({
     "name",
     "cache_ttl",
     "dataset",
+    "type",
     "dataset_query",
     "display",
     "description",
diff --git a/frontend/src/metabase/models/components/ModelDetailLink/ModelDetailLink.tsx b/frontend/src/metabase/models/components/ModelDetailLink/ModelDetailLink.tsx
index 85c79e261e8bc6079d65d2e212f1570c28b8cca5..bd91080f6f6cf86e6719e1d354b7123fab5a2a7d 100644
--- a/frontend/src/metabase/models/components/ModelDetailLink/ModelDetailLink.tsx
+++ b/frontend/src/metabase/models/components/ModelDetailLink/ModelDetailLink.tsx
@@ -8,10 +8,19 @@ import * as Urls from "metabase/lib/urls";
 
 import type { Card, CollectionItem } from "metabase-types/api";
 
-type ModelCard = Card & { dataset: true };
+type ModelCard = Card & { dataset: true; type: "model" };
+
+/**
+ * Omitting the "type" attribute is hopefully a temporary workaround
+ * until Metrics v2 are supported in Collections and the ambiguity between
+ * CollectionItem["type"] and Card["type"] disappears.
+ *
+ * @see https://github.com/metabase/metabase/issues/37350#issuecomment-1910284020
+ */
+type ModelCollectionItem = Omit<CollectionItem, "type">;
 
 interface Props extends ButtonProps {
-  model: ModelCard | CollectionItem;
+  model: ModelCard | ModelCollectionItem;
 }
 
 function ModelDetailLink({ model, ...props }: Props) {
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 7ae4333ad4c0f0cb73b50d16cafce4f25651b2d6..7cc09393c2a5b86e6f52bfa65ab56cd3d489df6d 100644
--- a/frontend/src/metabase/parameters/utils/mapping-options.unit.spec.js
+++ b/frontend/src/metabase/parameters/utils/mapping-options.unit.spec.js
@@ -50,7 +50,7 @@ describe("parameters/utils/mapping-options", () => {
         const question = ordersTable.question();
         dataset = question
           .setCard({ ...question.card(), id: 123 })
-          .setDataset(true);
+          .setType("model");
 
         // create a virtual table for the card
         // that contains fields with custom, model-specific metadata
diff --git a/frontend/src/metabase/query_builder/actions/core/core.js b/frontend/src/metabase/query_builder/actions/core/core.js
index 4461c35231409dd54c549263984786f6919aa086..7ba01e4e09e5956db51ded427eac7782de3d050c 100644
--- a/frontend/src/metabase/query_builder/actions/core/core.js
+++ b/frontend/src/metabase/query_builder/actions/core/core.js
@@ -175,7 +175,9 @@ export const navigateToNewCardInsideQB = createThunkAction(
           }
           // When the dataset query changes, we should loose the dataset flag,
           // to start building a new ad-hoc question based on a dataset
-          dispatch(setCardAndRun({ ...card, dataset: false }));
+          dispatch(
+            setCardAndRun({ ...card, dataset: false, type: "question" }),
+          );
         }
         if (objectId !== undefined) {
           dispatch(zoomInRow({ objectId }));
@@ -224,7 +226,7 @@ export const apiCreateQuestion = question => {
     MetabaseAnalytics.trackStructEvent(
       "QueryBuilder",
       "Create Card",
-      createdQuestion.type(),
+      createdQuestion.datasetQuery().type,
     );
     trackNewQuestionSaved(
       question,
@@ -295,7 +297,7 @@ export const apiUpdateQuestion = (question, { rerunQuery } = {}) => {
     MetabaseAnalytics.trackStructEvent(
       "QueryBuilder",
       "Update Card",
-      updatedQuestion.type(),
+      updatedQuestion.datasetQuery().type,
     );
 
     await dispatch({
diff --git a/frontend/src/metabase/query_builder/actions/core/updateQuestion.ts b/frontend/src/metabase/query_builder/actions/core/updateQuestion.ts
index 819fc693bf394e829240cac238a27907a16b2d74..dcca58e86843758385eacd4030cb0523800ad181 100644
--- a/frontend/src/metabase/query_builder/actions/core/updateQuestion.ts
+++ b/frontend/src/metabase/query_builder/actions/core/updateQuestion.ts
@@ -127,10 +127,10 @@ export const updateQuestion = (
     if (shouldTurnIntoAdHoc) {
       newQuestion = newQuestion.withoutNameAndId();
 
-      // When the dataset query changes, we should loose the dataset flag,
+      // When the dataset query changes, we should change the question type,
       // to start building a new ad-hoc question based on a dataset
       if (newQuestion.isDataset()) {
-        newQuestion = newQuestion.setDataset(false);
+        newQuestion = newQuestion.setType("question");
         dispatch(onCloseQuestionInfo());
       }
     }
@@ -139,7 +139,7 @@ export const updateQuestion = (
     // so that its query is shown properly in the notebook editor. Various child components of the notebook editor have access to
     // this `updateQuestion` action, so they end up triggering the action with the altered question.
     if (queryBuilderMode === "dataset" && !newQuestion.isDataset()) {
-      newQuestion = newQuestion.setDataset(true);
+      newQuestion = newQuestion.setType("model");
     }
 
     const queryResult = getFirstQueryResult(getState());
diff --git a/frontend/src/metabase/query_builder/actions/models.js b/frontend/src/metabase/query_builder/actions/models.js
index c219c1b084738d999b745f1055af929603e986ea..8020703a1630054d962085d858d19934cd2e1b36 100644
--- a/frontend/src/metabase/query_builder/actions/models.js
+++ b/frontend/src/metabase/query_builder/actions/models.js
@@ -42,7 +42,7 @@ export const turnQuestionIntoDataset = () => async (dispatch, getState) => {
       {
         id: question.id(),
       },
-      question.setDataset(true).setPinned(true).setDisplay("table").card(),
+      question.setType("model").setPinned(true).setDisplay("table").card(),
     ),
   );
 
@@ -69,7 +69,7 @@ export const turnQuestionIntoDataset = () => async (dispatch, getState) => {
 
 export const turnDatasetIntoQuestion = () => async (dispatch, getState) => {
   const dataset = getQuestion(getState());
-  const question = dataset.setDataset(false);
+  const question = dataset.setType("question");
   await dispatch(apiUpdateQuestion(question, { rerunQuery: true }));
 
   dispatch(
diff --git a/frontend/src/metabase/query_builder/actions/querying.js b/frontend/src/metabase/query_builder/actions/querying.js
index de1a04e218d3392a1d1b15883277de2322ed23d0..2d4b4a507ca24a8a98e61d2fcfef515e690c7236 100644
--- a/frontend/src/metabase/query_builder/actions/querying.js
+++ b/frontend/src/metabase/query_builder/actions/querying.js
@@ -132,7 +132,7 @@ export const runQuestionQuery = ({
           MetabaseAnalytics.trackStructEvent(
             "QueryBuilder",
             "Run Query",
-            question.type(),
+            question.datasetQuery().type,
             duration,
           ),
         );
diff --git a/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetQueryEditor.jsx b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetQueryEditor.jsx
index cd4bd74619fbfa3f73a6f12763c84907d5468aef..180d8c247f317067616dd1cf00b55cc12cfe65cd 100644
--- a/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetQueryEditor.jsx
+++ b/frontend/src/metabase/query_builder/components/DatasetEditor/DatasetQueryEditor.jsx
@@ -28,7 +28,7 @@ function DatasetQueryEditor({
 }) {
   // Datasets/models by default behave like they are already nested,
   // so we need to edit the dataset/model question like it is a normal question
-  const question = dataset.setDataset(false);
+  const question = dataset.setType("question");
   const { isNative } = Lib.queryDisplayInfo(question.query());
 
   const [isResizing, setResizing] = useState(false);
diff --git a/frontend/test/metabase-lib/lib/queries/StructuredQuery-nesting.unit.spec.js b/frontend/test/metabase-lib/lib/queries/StructuredQuery-nesting.unit.spec.js
index 45a51b5a94574a76b4decd04ae0aa65d01711b2a..38c7cd4cdd2f5488a9c399812105de974ac2c08b 100644
--- a/frontend/test/metabase-lib/lib/queries/StructuredQuery-nesting.unit.spec.js
+++ b/frontend/test/metabase-lib/lib/queries/StructuredQuery-nesting.unit.spec.js
@@ -139,7 +139,7 @@ describe("StructuredQuery nesting", () => {
 
       const metadata = ordersTable.metadata;
       const question = ordersTable.question();
-      const dataset = question.setId(1).setDataset(true);
+      const dataset = question.setId(1).setType("model");
       const nestedDatasetQuery = dataset
         .composeDataset()
         .legacyQuery({ useStructuredQuery: true });
diff --git a/resources/migrations/001_update_migrations.yaml b/resources/migrations/001_update_migrations.yaml
index d3e3be7fed6d5b2ae6d92118f42e3fcd015b447a..5a9ed7b612d47564e7feea631ca221e496567d4f 100644
--- a/resources/migrations/001_update_migrations.yaml
+++ b/resources/migrations/001_update_migrations.yaml
@@ -5228,6 +5228,42 @@ databaseChangeLog:
             tableName: metabase_field
             indexName: idx_field_name_lower
 
+  - changeSet:
+      id: v49.2024-01-22T11:50:00
+      author: qnkhuat
+      comment: Add `report_card.type`
+      changes:
+        - addColumn:
+            tableName: report_card
+            columns:
+              - column:
+                  remarks: The type of card, could be 'question', 'model', 'metric'
+                  name: type
+                  type: varchar(16)
+                  defaultValue: "question"
+                  constraints:
+                    nullable: false
+
+  - changeSet:
+      id: v49.2024-01-22T11:51:00
+      author: qnkhuat
+      comment: Backfill `report_card.type`
+      changes:
+        - sql:
+            sql: >-
+              UPDATE report_card
+              SET type = 'model'
+              WHERE dataset is true
+      rollback: # no need, the column will be dropped
+
+  - changeSet:
+      id: v49.2024-01-22T11:52:00
+      author: qnkhuat
+      comment: Backfill `report_card.type`
+      changes:
+        - customChange:
+            class: "metabase.db.custom_migrations.CardRevisionAddType"
+
   - changeSet:
       id: v49.2024-01-29T19:26:40
       author: adam-james
diff --git a/src/metabase/api/card.clj b/src/metabase/api/card.clj
index 7ae1b5e046668e1982f4f7a46e5bda9b2d90f54c..2b4e9af917ef748a79ecff7dd28a8919161f016c 100644
--- a/src/metabase/api/card.clj
+++ b/src/metabase/api/card.clj
@@ -155,8 +155,8 @@
                     :parameter_usage_count
                     [:collection :is_personal]
                     [:moderation_reviews :moderator_details])
-        (cond->
-          (:dataset card) (t2/hydrate :persisted)))))
+        (cond->                                             ; card
+          (card/model? card) (t2/hydrate :persisted)))))
 
 (api/defendpoint GET "/:id"
   "Get `Card` with ID."
@@ -384,12 +384,14 @@
 
 ;;; ------------------------------------------------- Creating Cards -------------------------------------------------
 
+
 (api/defendpoint POST "/"
   "Create a new `Card`."
   [:as {{:keys [collection_id collection_position dataset dataset_query description display name
-                parameters parameter_mappings result_metadata visualization_settings cache_ttl], :as body} :body}]
+                parameters parameter_mappings result_metadata visualization_settings cache_ttl type], :as body} :body}]
   {name                   ms/NonBlankString
    dataset                [:maybe :boolean]
+   type                   [:maybe card/CardTypes]
    dataset_query          ms/Map
    parameters             [:maybe [:sequential ms/Parameter]]
    parameter_mappings     [:maybe [:sequential ms/ParameterMapping]]
@@ -441,13 +443,14 @@
   "Update a `Card`."
   [id :as {{:keys [dataset_query description display name visualization_settings archived collection_id
                    collection_position enable_embedding embedding_params result_metadata parameters
-                   cache_ttl dataset collection_preview]
+                   cache_ttl dataset collection_preview type]
             :as   card-updates} :body}]
   {id                     ms/PositiveInt
    name                   [:maybe ms/NonBlankString]
    parameters             [:maybe [:sequential ms/Parameter]]
    dataset_query          [:maybe ms/Map]
    dataset                [:maybe :boolean]
+   type                   [:maybe card/CardTypes]
    display                [:maybe ms/NonBlankString]
    description            [:maybe :string]
    visualization_settings [:maybe ms/Map]
@@ -459,8 +462,11 @@
    result_metadata        [:maybe qr/ResultsMetadata]
    cache_ttl              [:maybe ms/PositiveInt]
    collection_preview     [:maybe :boolean]}
-  (let [card-before-update (t2/hydrate (api/write-check Card id)
-                                       [:moderation_reviews :moderator_details])]
+  (let [card-before-update     (t2/hydrate (api/write-check Card id)
+                                           [:moderation_reviews :moderator_details])
+        is-model-after-update? (if (and (nil? type) (nil? dataset))
+                                 (card/model? card-before-update)
+                                 (card/model? (card/ensure-type-and-dataset-are-consistent card-updates)))]
     ;; Do various permissions checks
     (doseq [f [collection/check-allowed-to-change-collection
                check-allowed-to-modify-query
@@ -471,11 +477,10 @@
                                                              :query             dataset_query
                                                              :metadata          result_metadata
                                                              :original-metadata (:result_metadata card-before-update)
-                                                             :dataset?          (if (some? dataset)
-                                                                                  dataset
-                                                                                  (:dataset card-before-update))})
+                                                             :dataset?          is-model-after-update?})
           card-updates          (merge card-updates
-                                       (when dataset
+                                       (when (and (or (some? type) (some? dataset))
+                                                  is-model-after-update?)
                                          {:display :table}))
           metadata-timeout      (a/timeout card/metadata-sync-wait-ms)
           [fresh-metadata port] (a/alts!! [result-metadata-chan metadata-timeout])
@@ -703,7 +708,7 @@
   [card-id]
   {card-id ms/PositiveInt}
   (premium-features/assert-has-feature :cache-granular-controls (tru "Granular cache controls"))
-  (api/let-404 [{:keys [dataset database_id] :as card} (t2/select-one Card :id card-id)]
+  (api/let-404 [{:keys [database_id] :as card} (t2/select-one Card :id card-id)]
     (let [database (t2/select-one Database :id database_id)]
       (api/write-check database)
       (when-not (driver/database-supports? (:engine database)
@@ -716,7 +721,7 @@
         (throw (ex-info (tru "Persisting models not enabled for database")
                         {:status-code 400
                          :database    (:name database)})))
-      (when-not dataset
+      (when-not (card/model? card)
         (throw (ex-info (tru "Card is not a model") {:status-code 400})))
       (when-let [persisted-info (persisted-info/turn-on-model! api/*current-user-id* card)]
         (task.persist-refresh/schedule-refresh-for-individual! persisted-info))
@@ -728,7 +733,7 @@
   {card-id ms/PositiveInt}
   (api/let-404 [card           (t2/select-one Card :id card-id)
                 persisted-info (t2/select-one PersistedInfo :card_id card-id)]
-    (when (not (:dataset card))
+    (when (not (card/model? card))
       (throw (ex-info (trs "Cannot refresh a non-model question") {:status-code 400})))
     (when (:archived card)
       (throw (ex-info (trs "Cannot refresh an archived model") {:status-code 400})))
diff --git a/src/metabase/db/custom_migrations.clj b/src/metabase/db/custom_migrations.clj
index 84952d1b0a69f5c449607ebf2476177613ec640d..68878dd4bd3dd1bf794824665c530f95ca36ecd5 100644
--- a/src/metabase/db/custom_migrations.clj
+++ b/src/metabase/db/custom_migrations.clj
@@ -1033,3 +1033,78 @@
 (define-reversible-migration UnifyTimeColumnsType
   (unify-time-column-type! :up)
   (unify-time-column-type! :down))
+
+(define-reversible-migration CardRevisionAddType
+  (case (mdb.connection/db-type)
+    :postgres
+    (t2/query ["UPDATE revision
+               SET object = jsonb_set(
+                  object::jsonb, '{type}',
+                  to_jsonb(CASE
+                              WHEN (object::jsonb->>'dataset')::boolean THEN 'model'
+                              ELSE 'question'
+                           END)::jsonb, true)
+               WHERE model = 'Card' AND (object::jsonb->>'dataset') IS NOT NULL;"])
+
+    :mysql
+    (t2/query ["UPDATE revision
+               SET object = JSON_SET(
+                   object,
+                   '$.type',
+                   CASE
+                       WHEN JSON_UNQUOTE(JSON_EXTRACT(object, '$.dataset')) = 'true' THEN 'model'
+                       ELSE 'question'
+                   END)
+               WHERE model = 'Card' AND JSON_UNQUOTE(JSON_EXTRACT(object, '$.dataset')) IS NOT NULL;;"])
+    :h2
+    (let [migrate! (fn [revision]
+                     (let [object     (json/parse-string (:object revision) keyword)
+                           new-object (assoc object :type (if (:dataset object)
+                                                            "model"
+                                                            "question"))]
+                       (t2/query {:update :revision
+                                  :set    {:object (json/generate-string new-object)}
+                                  :where  [:= :id (:id revision)]})))]
+      (run! migrate! (t2/reducible-query {:select [:*]
+                                          :from   [:revision]
+                                          :where  [:= :model "Card"]}))))
+  (case (mdb.connection/db-type)
+    :postgres
+    (t2/query ["UPDATE revision
+                SET object = jsonb_set(
+                    object::jsonb - 'type',
+                    '{dataset}',
+                    to_jsonb(CASE
+                                 WHEN (object::jsonb->>'type') = 'model'
+                                 THEN true ELSE false
+                             END)
+                )
+                WHERE model = 'Card' AND (object::jsonb->>'type') IS NOT NULL;"])
+
+    :mysql
+    (do
+     (t2/query ["UPDATE revision
+                 SET object = JSON_SET(
+                     object,
+                     '$.dataset',
+                     CASE
+                         WHEN JSON_UNQUOTE(JSON_EXTRACT(object, '$.type')) = 'model'
+                         THEN true ELSE false
+                     END)
+                 WHERE model = 'Card' AND JSON_UNQUOTE(JSON_EXTRACT(object, '$.type')) IS NOT NULL;"])
+     (t2/query ["UPDATE revision
+                 SET object = JSON_REMOVE(object, '$.type')
+                 WHERE model = 'Card' AND JSON_UNQUOTE(JSON_EXTRACT(object, '$.type')) IS NOT NULL;"]))
+
+    :h2
+    (let [rollback! (fn [revision]
+                      (let [object     (json/parse-string (:object revision) keyword)
+                            new-object (-> object
+                                           (assoc :dataset (= (:type object) "model"))
+                                           (dissoc :type))]
+                        (t2/query {:update :revision
+                                   :set    {:object (json/generate-string new-object)}
+                                   :where  [:= :id (:id revision)]})))]
+      (run! rollback! (t2/reducible-query {:select [:*]
+                                           :from   [:revision]
+                                           :where  [:= :model "Card"]})))))
diff --git a/src/metabase/models/card.clj b/src/metabase/models/card.clj
index fcdc7a316b046ee210a21fd693fa6961c00d2fc6..580a3c1ee6a795762198128ac5db4f70a3a0348f 100644
--- a/src/metabase/models/card.clj
+++ b/src/metabase/models/card.clj
@@ -44,6 +44,7 @@
    [metabase.util :as u]
    [metabase.util.i18n :refer [tru]]
    [metabase.util.log :as log]
+   [metabase.util.malli :as mu]
    [methodical.core :as methodical]
    [schema.core :as s]
    [toucan2.core :as t2]
@@ -101,6 +102,38 @@
   ([_ pk]
    (mi/can-read? (t2/select-one :model/Card :id pk))))
 
+(def ^{:private true
+       :doc     "All acceptable card types.
+                Previously (< 49), we only had 2 card types: question and model, which were differentiated using the
+                boolean `dataset` column. Soon we'll have more card types (e.g: metric) and we will longer be able to use a boolean
+                column to differentiate between all types. So we've added a new `type` column for this purpose.
+
+                Migrating all the code to use `report_card.type` will be quite an effort, we decided that we'll migrate it gradually.
+                In the mean time we'll have both `type` and `dataset` columns."} card-types
+  #{"model" "question"
+    ;; metric will be added as part of epic #37335
+    #_"metric"})
+
+(def CardTypes
+  "Malli schema for acceptable card types."
+  (into [:enum] card-types))
+
+(mu/defn ^:private is-type? :- :boolean
+  "Returns true if card is of type `target-type`"
+  [target-type :- CardTypes
+   {:keys [type] :as _card} :- [:map [:type CardTypes]]]
+  (= target-type type))
+
+(defn question?
+  "Returns true if `card` is a question."
+  [card]
+  (is-type? "question" card))
+
+(defn model?
+  "Returns true if `card` is a model."
+  [card]
+  (is-type? "model" card))
+
 ;;; -------------------------------------------------- Hydration --------------------------------------------------
 
 (mi/define-simple-hydration-method dashboard-count
@@ -148,7 +181,9 @@
 ;;; --------------------------------------------------- Revisions ----------------------------------------------------
 
 (def ^:private excluded-columns-for-card-revision
-  [:id :created_at :updated_at :entity_id :creator_id :public_uuid :made_public_by_id :metabase_version])
+  [:id :created_at :updated_at :entity_id :creator_id :public_uuid :made_public_by_id :metabase_version
+   ;; we'll use type now
+   :dataset])
 
 (defmethod revision/serialize-instance :model/Card
   ([instance]
@@ -156,7 +191,8 @@
   ([_model _id instance]
    (cond-> (apply dissoc instance excluded-columns-for-card-revision)
      ;; datasets should preserve edits to metadata
-     (not (:dataset instance))
+     ;; the type check only needed in tests because most test object does not include `type` key
+     (and (some? (:type instance)) (not (model? instance)))
      (dissoc :result_metadata))))
 
 ;;; --------------------------------------------------- Lifecycle ----------------------------------------------------
@@ -306,15 +342,58 @@
                            :query-database        query-db-id
                            :field-filter-database field-db-id})))))))
 
-(defn- assert-valid-model
+(defn- assert-card-type-and-dataset
+  "Throw an exception if card type and dataset contradicts, return the card if it's not."
+  [{:keys [type dataset] :as card}]
+  (if (and (some? type) (some? dataset)
+           (if (true? dataset)
+             (not= "model" type)
+             (= "model" type)))
+    (throw (ex-info (tru ":dataset is inconsistent with :type")
+                    {:status-code 400}))
+    card))
+
+(defn ensure-type-and-dataset-are-consistent
+  "We're in the process of migrating from using `report_card.dataset` to `report_card.type`.
+  In the future we'll drop `dataset` and only use `type`. But for now we need to make sure that both keys are aligned
+  when dealing with cards.
+  - If both keys are present, throw an exception if type and dataset is inconsistent.
+    If not we make sure `dataset` is true if `type` is `model` else false.
+  This will make a different when we have `metric` type since a boolean can't represent tri-state
+  - If only one key is present, we'll assoc the correct value for the other key."
+  [{:keys [type dataset] :as card}]
+  (cond
+   ;; if none of the 2 keys is present, do nothing
+   (and (nil? type) (nil? dataset))
+   card
+
+   ;; if both type and dataset is present, makes sure they don't contradict
+   (and (some? type) (some? dataset))
+   (assert-card-type-and-dataset card)
+
+   ;; if only type is present, make sure dataset follows
+   (some? type)
+   (assoc card :dataset (= type "model"))
+
+   ;; if only dataset is present, make sure type follows
+   (some? dataset)
+   (let [inferred-type (if dataset
+                         "model"
+                         "question")]
+     (log/warnf "Card type not found, defaulting to '%s'" inferred-type)
+     (assoc card :type inferred-type))))
+
+(defn- assert-valid-type
   "Check that the card is a valid model if being saved as one. Throw an exception if not."
-  [{:keys [dataset dataset_query]}]
-  (when dataset
-    (let [template-tag-types (->> (vals (get-in dataset_query [:native :template-tags]))
-                                  (map (comp keyword :type)))]
-      (when (some (complement #{:card :snippet}) template-tag-types)
-        (throw (ex-info (tru "A model made from a native SQL question cannot have a variable or field filter.")
-                        {:status-code 400}))))))
+  [{:keys [type dataset_query]}]
+  (case type
+    "model" (let [template-tag-types (->> (get-in dataset_query [:native :template-tags])
+                                          vals
+                                          (map (comp keyword :type)))]
+              (when (some (complement #{:card :snippet}) template-tag-types)
+                (throw (ex-info (tru "A model made from a native SQL question cannot have a variable or field filter.")
+                                {:status-code 400}))))
+    nil))
 
 ;; TODO -- consider whether we should validate the Card query when you save/update it??
 (defn- pre-insert [card]
@@ -326,7 +405,7 @@
       (check-for-circular-source-query-references card)
       (check-field-filter-fields-are-from-correct-database card)
       ;; TODO: add a check to see if all id in :parameter_mappings are in :parameters
-      (assert-valid-model card)
+      (assert-valid-type card)
       (params/assert-valid-parameters card)
       (params/assert-valid-parameter-mappings card)
       (collection/check-collection-namespace Card (:collection_id card)))))
@@ -444,7 +523,7 @@
       (parameter-card/upsert-or-delete-from-parameters! "card" id (:parameters changes))
       ;; additional checks (Enterprise Edition only)
       (pre-update-check-sandbox-constraints changes)
-      (assert-valid-model (merge old-card-info changes)))))
+      (assert-valid-type (merge old-card-info changes)))))
 
 (t2/define-after-select :model/Card
   [card]
@@ -453,6 +532,7 @@
 (t2/define-before-insert :model/Card
   [card]
   (-> card
+      ensure-type-and-dataset-are-consistent
       (assoc :metabase_version config/mb-version-string)
       maybe-normalize-query
       populate-result-metadata
@@ -475,6 +555,7 @@
   ;; We have to convert this to a plain map rather than a Toucan 2 instance at this point to work around upstream bug
   ;; https://github.com/camsaul/toucan2/issues/145 .
   (-> (into {:id (:id card)} (t2/changes card))
+      ensure-type-and-dataset-are-consistent
       maybe-normalize-query
       populate-result-metadata
       pre-update
@@ -594,19 +675,23 @@ saved later when it is ready."
   the transaction yet. If you pass true here it is important to call the event after the cards are successfully
   created."
   ([card creator] (create-card! card creator false))
-  ([{:keys [dataset_query result_metadata dataset parameters parameter_mappings], :as card-data} creator delay-event?]
-   ;; `zipmap` instead of `select-keys` because we want to get `nil` values for keys that aren't present. Required by
-   ;; `api/maybe-reconcile-collection-position!`
+  ([{:keys [dataset_query result_metadata dataset parameters parameter_mappings type] :as card-data} creator delay-event?]
+   (assert-card-type-and-dataset card-data)
    (let [data-keys            [:dataset_query :description :display :name :visualization_settings
-                               :parameters :parameter_mappings :collection_id :collection_position :cache_ttl]
-         card-data            (assoc (zipmap data-keys (map card-data data-keys))
-                                     :creator_id (:id creator)
-                                     :dataset (boolean (:dataset card-data))
-                                     :parameters (or parameters [])
-                                     :parameter_mappings (or parameter_mappings []))
+                               :parameters :parameter_mappings :collection_id :collection_position :cache_ttl :type :dataset]
+         ;; `zipmap` instead of `select-keys` because we want to get `nil` values for keys that aren't present. Required by
+         ;; `api/maybe-reconcile-collection-position!`
+         card-data            (-> (zipmap data-keys (map card-data data-keys))
+                                  (assoc
+                                   :creator_id (:id creator)
+                                   :parameters (or parameters [])
+                                   :parameter_mappings (or parameter_mappings []))
+                                  (cond-> (and (nil? type) (nil? dataset))
+                                    (assoc :type "question"))
+                                  ensure-type-and-dataset-are-consistent)
          result-metadata-chan (result-metadata-async {:query    dataset_query
                                                       :metadata result_metadata
-                                                      :dataset? dataset})
+                                                      :dataset? (model? card-data)})
          metadata-timeout     (a/timeout metadata-sync-wait-ms)
          [metadata port]      (a/alts!! [result-metadata-chan metadata-timeout])
          timed-out?           (= port metadata-timeout)
@@ -614,9 +699,9 @@ saved later when it is ready."
                                 ;; Adding a new card at `collection_position` could cause other cards in this
                                 ;; collection to change position, check that and fix it if needed
                                 (api/maybe-reconcile-collection-position! card-data)
-                                (first (t2/insert-returning-instances! Card (cond-> card-data
-                                                                              (and metadata (not timed-out?))
-                                                                              (assoc :result_metadata metadata)))))]
+                                (t2/insert-returning-instance! Card (cond-> card-data
+                                                                      (and metadata (not timed-out?))
+                                                                      (assoc :result_metadata metadata))))]
      (when-not delay-event?
        (events/publish-event! :event/card-create {:object card :user-id (:id creator)}))
      (when timed-out?
@@ -757,26 +842,27 @@ saved later when it is ready."
   included, otherwise the metadata will be saved to the database asynchronously."
   [{:keys [card-before-update card-updates actor]}]
   ;; don't block our precious core.async thread, run the actual DB updates on a separate thread
-  (t2/with-transaction [_conn]
-   (api/maybe-reconcile-collection-position! card-before-update card-updates)
-
-   (when (and (card-is-verified? card-before-update)
-              (changed? card-compare-keys card-before-update card-updates))
-     ;; this is an enterprise feature but we don't care if enterprise is enabled here. If there is a review we need
-     ;; to remove it regardless if enterprise edition is present at the moment.
-     (moderation-review/create-review! {:moderated_item_id   (:id card-before-update)
-                                        :moderated_item_type "card"
-                                        :moderator_id        (:id actor)
-                                        :status              nil
-                                        :text                (tru "Unverified due to edit")}))
-   ;; ok, now save the Card
-   (t2/update! Card (:id card-before-update)
-     ;; `collection_id` and `description` can be `nil` (in order to unset them). Other values should only be
-     ;; modified if they're passed in as non-nil
-     (u/select-keys-when card-updates
-       :present #{:collection_id :collection_position :description :cache_ttl :dataset}
-       :non-nil #{:dataset_query :display :name :visualization_settings :archived :enable_embedding
-                  :parameters :parameter_mappings :embedding_params :result_metadata :collection_preview})))
+  (let [card-updates (ensure-type-and-dataset-are-consistent card-updates)]
+    (t2/with-transaction [_conn]
+      (api/maybe-reconcile-collection-position! card-before-update card-updates)
+
+      (when (and (card-is-verified? card-before-update)
+                 (changed? card-compare-keys card-before-update card-updates))
+        ;; this is an enterprise feature but we don't care if enterprise is enabled here. If there is a review we need
+        ;; to remove it regardless if enterprise edition is present at the moment.
+        (moderation-review/create-review! {:moderated_item_id   (:id card-before-update)
+                                           :moderated_item_type "card"
+                                           :moderator_id        (:id actor)
+                                           :status              nil
+                                           :text                (tru "Unverified due to edit")}))
+      ;; ok, now save the Card
+      (t2/update! Card (:id card-before-update)
+                  ;; `collection_id` and `description` can be `nil` (in order to unset them). Other values should only be
+                  ;; modified if they're passed in as non-nil
+                  (u/select-keys-when card-updates
+                                      :present #{:collection_id :collection_position :description :cache_ttl :dataset :type}
+                                      :non-nil #{:dataset_query :display :name :visualization_settings :archived :enable_embedding
+                                                 :parameters :parameter_mappings :embedding_params :result_metadata :collection_preview}))))
   ;; Fetch the updated Card from the DB
   (let [card (t2/select-one Card :id (:id card-before-update))]
     (delete-alerts-if-needed! :old-card card-before-update, :new-card card, :actor actor)
@@ -792,7 +878,7 @@ saved later when it is ready."
   (serdes/extract-query-collections Card opts))
 
 (defn- export-result-metadata [card metadata]
-  (when (and metadata (:dataset card))
+  (when (and metadata (model? card))
     (for [m metadata]
       (-> (dissoc m :fingerprint)
           (m/update-existing :table_id  serdes/*export-table-fk*)
diff --git a/src/metabase/models/revision.clj b/src/metabase/models/revision.clj
index 389727c31ed012b5d896bb99da92c1f559d0d76f..95f79810424db9dc2960f1f8f8a7a65e84e889e7 100644
--- a/src/metabase/models/revision.clj
+++ b/src/metabase/models/revision.clj
@@ -32,7 +32,7 @@
 
 (defmethod revert-to-revision! :default
   [model id _user-id serialized-instance]
-  (t2/update! model id, serialized-instance))
+  (t2/update! model id serialized-instance))
 
 (defmulti diff-map
   "Return a map describing the difference between `object-1` and `object-2`."
diff --git a/src/metabase/models/revision/diff.clj b/src/metabase/models/revision/diff.clj
index 4d908fa0dceb96182268b3467baa1b0397775155..d7c3043be40110c7a915ba980aec98196fa77761 100644
--- a/src/metabase/models/revision/diff.clj
+++ b/src/metabase/models/revision/diff.clj
@@ -76,11 +76,12 @@
     [:dataset_query _ _]
     (deferred-tru "modified the query")
 
-    [:dataset false true]
-    (deferred-tru "turned this into a model")
+    ;; report_card.type
+    [:type "question" "model"]
+    (deferred-tru "turned this to a model")
 
-    [:dataset true false]
-    (deferred-tru "changed this from a model to a saved question")
+    [:type old new]
+    (deferred-tru "type changed from {0} to {1}" old new)
 
     [:display _ _]
     (deferred-tru "changed the display from {0} to {1}" (name v1) (name v2))
diff --git a/test/metabase/api/card_test.clj b/test/metabase/api/card_test.clj
index 0d88058b1bf885a474a772b35198b9ff56295cf1..d6859b45084302bfc8fa0f76850ae619951e30dc 100644
--- a/test/metabase/api/card_test.clj
+++ b/test/metabase/api/card_test.clj
@@ -72,6 +72,7 @@
    :collection_preview  true
    :dataset_query       {}
    :dataset             false
+   :type                "question"
    :description         nil
    :display             "scalar"
    :enable_embedding    false
@@ -978,6 +979,59 @@
                          [:trace          [:sequential :any]]]
                         (create-card! :rasta 403)))))))))
 
+(deftest create-card-with-type-and-dataset-test
+  (mt/with-model-cleanup [:model/Card]
+    (testing "type and dataset must match"
+      (is (= ":dataset is inconsistent with :type"
+             (mt/user-http-request :crowberto :post 400 "card" (assoc (card-with-name-and-query (mt/random-name))
+                                                                      :dataset true
+                                                                      :type :question))))
+      (is (= ":dataset is inconsistent with :type"
+             (mt/user-http-request :crowberto :post 400 "card" (assoc (card-with-name-and-query (mt/random-name))
+                                                                      :dataset false
+                                                                      :type "model")))))
+    (testing "can create a model using dataset"
+      (is (=? {:dataset true
+               :type    "model"}
+              (mt/user-http-request :crowberto :post 200 "card" (assoc (card-with-name-and-query (mt/random-name))
+                                                                       :dataset true)))))
+
+    (testing "can create a model using type"
+      (is (=? {:dataset true
+               :type    "model"}
+              (mt/user-http-request :crowberto :post 200 "card" (assoc (card-with-name-and-query (mt/random-name))
+                                                                       :type :model)))))
+
+    (testing "default is a question"
+      (is (=? {:dataset false
+               :type    "question"}
+              (mt/user-http-request :crowberto :post 200 "card" (card-with-name-and-query (mt/random-name))))))))
+
+(deftest update-card-with-type-and-dataset-test
+  (testing "can toggle model using only type"
+    (mt/with-temp [:model/Card card {:dataset_query {}}]
+      (is (=? {:dataset true
+               :type    "model"}
+              (mt/user-http-request :crowberto :put 200 (str "card/" (:id card)) {:type "model"})))
+
+      (is (=? {:dataset false
+               :type    "question"}
+              (mt/user-http-request :crowberto :put 200 (str "card/" (:id card)) {:type "question"})))))
+
+  (testing "can toggle model using both type and dataset"
+    (mt/with-temp [:model/Card card {:dataset_query {}}]
+      (is (=? {:dataset true
+               :type    "model"}
+              (mt/user-http-request :crowberto :put 200 (str "card/" (:id card)) {:type "model" :dataset true})))
+
+      (is (=? {:dataset false
+               :type    "question"}
+              (mt/user-http-request :crowberto :put 200 (str "card/" (:id card)) {:type "question" :dataset false})))
+
+      (testing "but error if type and dataset doesn't match"
+        (is (= ":dataset is inconsistent with :type"
+               (mt/user-http-request :crowberto :put 400 (str "card/" (:id card)) {:type "question" :dataset true})))))))
+
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                    COPYING A CARD (POST /api/card/:id/copy)                                    |
 ;;; +----------------------------------------------------------------------------------------------------------------+
@@ -1250,6 +1304,16 @@
         (is (= "A model made from a native SQL question cannot have a variable or field filter."
                (mt/user-http-request :rasta :put 400 (format "card/%d" (:id card)) {:dataset true})))))))
 
+(deftest ^:parallel turn-card-to-model-change-display-test
+  (mt/with-temp [:model/Card card {:display :line}]
+    (is (=? {:display "table"}
+            (mt/user-http-request :crowberto :put 200 (str "card/" (:id card))
+                                  {:dataset true}))))
+
+  (mt/with-temp [:model/Card card {:display :line}]
+    (is (=? {:display "table"}
+            (mt/user-http-request :crowberto :put 200 (str "card/" (:id card))
+                                  {:type "model"})))))
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                        Updating the positions of stuff                                         |
@@ -2647,7 +2711,7 @@
                                                :dataset_query (mbql-count-query)}]
       (is (=? {:display "table" :dataset true}
               (mt/user-http-request :crowberto :put 200 (str "card/" (u/the-id card))
-                                    (assoc card :dataset true)))))))
+                                    (assoc card :dataset true :type "model")))))))
 
 (deftest dataset-card-2
   (testing "Cards preserve their edited metadata"
diff --git a/test/metabase/api/revision_test.clj b/test/metabase/api/revision_test.clj
index c87f8aa25ad7b26b42b9e9e1025aaba9a9d3e7e7..54cc8ce5e8e7ad6de8c210132a21e00a98dcb0ef 100644
--- a/test/metabase/api/revision_test.clj
+++ b/test/metabase/api/revision_test.clj
@@ -326,7 +326,7 @@
       (create-card-revision! card-id false :crowberto)
 
       ;; 2. turn to a model
-      (t2/update! Card :id card-id {:dataset true})
+      (t2/update! Card :id card-id {:type "model"})
       (create-card-revision! card-id false :crowberto)
 
       ;; 3. edit query and metadata
@@ -355,7 +355,7 @@
                :has_multiple_changes false}
               {:description          "changed the display from table to scalar, modified the query and edited the metadata."
                :has_multiple_changes true}
-              {:description          "turned this into a model and edited the metadata."
+              {:description          "turned this to a model and edited the metadata."
                :has_multiple_changes true}
               {:description          "renamed this Card from \"A card\" to \"New name\"."
                :has_multiple_changes false}
diff --git a/test/metabase/api/search_test.clj b/test/metabase/api/search_test.clj
index 5fe0061607ea889293afe23009aa8e7e36b946d0..824b5931769ff44e99adf93daba9fc2a4e921a82 100644
--- a/test/metabase/api/search_test.clj
+++ b/test/metabase/api/search_test.clj
@@ -1096,7 +1096,9 @@
             :id           id
             :user-id      user-id
             :is-creation? true
-            :object       {:id id}}))
+            :object       (merge {:id id}
+                                 (when (= model :model/Card)
+                                   {:type "question"}))}))
 
         (testing "Able to filter by last editor"
           (let [resp (mt/user-http-request :crowberto :get 200 "search" :q search-term :last_edited_by rasta-user-id)]
@@ -1233,7 +1235,9 @@
           :id           id
           :user-id      (mt/user->id :rasta)
           :is-creation? true
-          :object       {:id id}}))
+          :object       (merge {:id id}
+                               (when (= model :model/Card)
+                                 {:type "question"}))}))
       (testing "returns only applicable models"
         (let [resp (mt/user-http-request :crowberto :get 200 "search" :q search-term :last_edited_at "today")]
           (is (= #{[action-id "action"]
@@ -1258,7 +1262,9 @@
             :id           id
             :user-id      (mt/user->id :rasta)
             :is-creation? true
-            :object       {:id id}}))
+            :object       (merge {:id id}
+                                 (when (= model :model/Card)
+                                   {:type "question"}))}))
         (is (= #{"dashboard" "dataset" "metric" "card"}
                (-> (mt/user-http-request :crowberto :get 200 "search" :q search-term :last_edited_at "today" :last_edited_by (mt/user->id :rasta))
                    :available_models
@@ -1513,14 +1519,14 @@
         :id           card-id-1
         :user-id      user-id-1
         :is-creation? true
-        :object       {:id card-id-1}})
+        :object       {:id card-id-1 :type "question"}})
 
       (revision/push-revision!
        {:entity       :model/Card
         :id           card-id-2
         :user-id      user-id-2
         :is-creation? true
-        :object       {:id card-id-2}})
+        :object       {:id card-id-2 :type "question"}})
 
       (testing "search result should returns creator_common_name and last_editor_common_name"
         (is (= #{["card" card-id-1 "Ngoc Khuat" "Ngoc Khuat"]
diff --git a/test/metabase/db/custom_migrations_test.clj b/test/metabase/db/custom_migrations_test.clj
index b56ff1905bbc3e5a249e117fb59828e0a5a46b1c..555fbb7ae04b8b269ec65af134e521f0f454812f 100644
--- a/test/metabase/db/custom_migrations_test.clj
+++ b/test/metabase/db/custom_migrations_test.clj
@@ -35,6 +35,33 @@
 
 (jobs/defjob AbandonmentEmail [_] :default)
 
+(defn- table-default [table]
+  (case table
+    :core_user         {:first_name  (mt/random-name)
+                        :last_name   (mt/random-name)
+                        :email       (mt/random-email)
+                        :password    "superstrong"
+                        :date_joined :%now}
+    :metabase_database {:name       (mt/random-name)
+                        :engine     "h2"
+                        :details    "{}"
+                        :created_at :%now
+                        :updated_at :%now}
+    :report_card       {:name                   (mt/random-name)
+                        :dataset_query          "{}"
+                        :display                "table"
+                        :visualization_settings "{}"
+                        :created_at             :%now
+                        :updated_at             :%now}
+    :revision          {:timestamp :%now}
+    {}))
+
+(defn- new-instance-with-default
+  ([table]
+   (new-instance-with-default table {}))
+  ([table properties]
+   (t2/insert-returning-instance! table (merge (table-default table) properties))))
+
 (deftest delete-abandonment-email-task-test
   (testing "Migration v46.00-086: Delete the abandonment email task"
     (impl/test-migrations ["v46.00-086"] [migrate!]
@@ -1326,13 +1353,15 @@
                                                                    :updated_at :%now
                                                                    :details    "{}"})
                 [card-id]      (t2/insert-returning-pks!
-                                :model/Card
+                                :report_card
                                 {:visualization_settings card-vis
                                  :display                "table"
                                  :dataset_query          "{}"
                                  :creator_id             user-id
                                  :database_id            database-id
-                                 :name                   "My Card"})
+                                 :name                   "My Card"
+                                 :created_at             :%now
+                                 :updated_at             :%now})
                 [dashboard-id] (t2/insert-returning-pks! :model/Dashboard {:name       "My Dashboard"
                                                                            :creator_id user-id
                                                                            :parameters []})
@@ -1511,3 +1540,42 @@
             (testing "created_at shouldn't change if there is an update"
               (is (= (:created_at session)
                      (t2/select-one-fn :created_at :core_session :id (:id session))))))))))
+
+(deftest card-revision-add-type-test
+  (impl/test-migrations "v49.2024-01-22T11:52:00" [migrate!]
+    (let [user-id          (:id (new-instance-with-default :core_user))
+          db-id            (:id (new-instance-with-default :metabase_database))
+          card             (new-instance-with-default :report_card {:dataset false :creator_id user-id :database_id db-id})
+          model            (new-instance-with-default :report_card {:dataset true :creator_id user-id :database_id db-id})
+          card-revision-id (:id (new-instance-with-default :revision
+                                                           {:object    (json/generate-string (dissoc card :type))
+                                                            :model     "Card"
+                                                            :model_id  (:id card)
+                                                            :user_id   user-id}))
+          model-revision-id (:id (new-instance-with-default :revision
+                                                            {:object    (json/generate-string (dissoc model :type))
+                                                             :model     "Card"
+                                                             :model_id  (:id card)
+                                                             :user_id   user-id}))]
+      (testing "sanity check revision object"
+        (let [card-revision-object (t2/select-one-fn (comp json/parse-string :object) :revision card-revision-id)]
+          (testing "doesn't have type"
+            (is (not (contains? card-revision-object "type"))))
+          (testing "has dataset"
+            (is (contains? card-revision-object "dataset")))))
+
+      (testing "after migration card revisions should have type"
+        (migrate!)
+        (let [card-revision-object  (t2/select-one-fn (comp json/parse-string :object) :revision card-revision-id)
+              model-revision-object (t2/select-one-fn (comp json/parse-string :object) :revision model-revision-id)]
+          (is (= "question" (get card-revision-object "type")))
+          (is (= "model" (get model-revision-object "type")))))
+
+      (testing "rollback should remove type and keep dataset"
+        (migrate! :down 48)
+        (let [card-revision-object  (t2/select-one-fn (comp json/parse-string :object) :revision card-revision-id)
+              model-revision-object (t2/select-one-fn (comp json/parse-string :object) :revision model-revision-id)]
+          (is (contains? card-revision-object "dataset"))
+          (is (contains? model-revision-object "dataset"))
+          (is (not (contains? card-revision-object "type")))
+          (is (not (contains? model-revision-object "type"))))))))
diff --git a/test/metabase/events/revision_test.clj b/test/metabase/events/revision_test.clj
index a271acf64558994add1755d2b5c11545b3c98686..35623717afb78859f7259367571f19c1010a7414 100644
--- a/test/metabase/events/revision_test.clj
+++ b/test/metabase/events/revision_test.clj
@@ -27,7 +27,7 @@
    :collection_preview     true
    :database_id            (mt/id)
    :dataset_query          (:dataset_query card)
-   :dataset                false
+   :type                   "question"
    :description            nil
    :display                :table
    :enable_embedding       false
diff --git a/test/metabase/models/card_test.clj b/test/metabase/models/card_test.clj
index ce7ef83dc1555ed65c96e1c868ef742534898cf7..b6d5da9e13ba640b51a39be07216abfb37af9e1a 100644
--- a/test/metabase/models/card_test.clj
+++ b/test/metabase/models/card_test.clj
@@ -760,6 +760,7 @@
                             (= col :table_id)          (mt/id :venues)
                             (= col :database_id)       (:id db)
                             (= col :query_type)        :native
+                            (= col :type)              "model"
                             (= col :dataset_query)     (mt/mbql-query users)
                             (= col :visualization_settings) {:text "now it's a text card"}
                             (int? value)               (inc value)
diff --git a/test/metabase/models/revision_test.clj b/test/metabase/models/revision_test.clj
index 998b5b068b52c48cd2a2b8bd74e7a1d63edce6fa..7c3915b909ef83a91d0b965f73b163bdb7173c71 100644
--- a/test/metabase/models/revision_test.clj
+++ b/test/metabase/models/revision_test.clj
@@ -92,12 +92,12 @@
               {:name "Spots by State", :private true})))))
 
   (testing "Check that several changes are handled nicely"
-    (is (= (str "turned this into a model, made it private and renamed it from \"Tips by State\" to \"Spots by State\".")
+    (is (= "turned this to a model, made it private and renamed it from \"Tips by State\" to \"Spots by State\"."
            (u/build-sentence
              ((get-method revision/diff-strings :default)
               Card
-              {:name "Tips by State", :private false, :dataset false}
-              {:name "Spots by State", :private true, :dataset true}))))))
+              {:name "Tips by State", :private false, :type "question"}
+              {:name "Spots by State", :private true, :type "model"}))))))
 
 (deftest ^:parallel revision-contains-changes-that-has-havent-been-specced-test
   (testing "When revision object contains key that we don't know how to generate diff-string
@@ -416,24 +416,24 @@
            {:description          "changed the display from table to bar and turned this into a model."
             :has_multiple_changes true}
            (#'revision/revision-description-info model
-                                                 {:object       {:dataset false
+                                                 {:object       {:type "question"
                                                                  :display :table}
                                                   :is_reversion false
                                                   :is_creation  false}
-                                                 {:object       {:dataset true
+                                                 {:object       {:type "model"
                                                                  :display :bar}
                                                   :is_reversion false
                                                   :is_creation  false}))
 
          (testing "changes contains unspecified keys will not be mentioned"
-           (is (= {:description          "turned this into a model."
+           (is (= {:description          "turned this to a model."
                    :has_multiple_changes false}
                   (#'revision/revision-description-info model
-                                                        {:object       {:dataset     false
+                                                        {:object       {:type        "question"
                                                                         :unknown_key false}
                                                          :is_reversion false
                                                          :is_creation  false}
-                                                        {:object       {:dataset     true
+                                                        {:object       {:type        "model"
                                                                         :unknown_key false}
                                                          :is_reversion false
                                                          :is_creation  false})))))))))