diff --git a/.clj-kondo/test/hooks/clojure/test_test.clj b/.clj-kondo/test/hooks/clojure/test_test.clj
index 19bf39b462102075526370191b98a63d2ba91b58..bcd487a1c905beffec5ec3d4a6ebdfb65da9f304 100644
--- a/.clj-kondo/test/hooks/clojure/test_test.clj
+++ b/.clj-kondo/test/hooks/clojure/test_test.clj
@@ -3,7 +3,6 @@
    [clj-kondo.hooks-api :as api]
    [clj-kondo.impl.utils]
    [clojure.edn :as edn]
-   [clojure.java.io :as io]
    [clojure.test :refer :all]
    [hooks.clojure.test]))
 
@@ -44,9 +43,13 @@
   (testing "Make sure we keep hooks.clojure.test/driver-keywords up to date"
     (let [driver-keywords (-> (slurp ".clj-kondo/config.edn")
                               edn/read-string
-                              (get-in [:linters :metabase/disallow-hardcoded-driver-names-in-tests :drivers]))]
-      (doseq [^java.io.File file (.listFiles (io/file "modules/drivers"))
-              :when (.isDirectory file)
-              :let [driver (keyword (.getName file))]]
+                              (get-in [:linters :metabase/disallow-hardcoded-driver-names-in-tests :drivers]))
+          driver-modules (->> (slurp "modules/drivers/deps.edn")
+                              edn/read-string
+                              :deps
+                              vals
+                              (keep (comp keyword :local/root))
+                              (into #{}))]
+      (doseq [driver driver-modules]
         (is (contains? driver-keywords driver)
             (format "hooks.clojure.test/driver-keywords should contain %s, please add it" driver))))))
diff --git a/docs/questions/query-builder/expressions-list.md b/docs/questions/query-builder/expressions-list.md
index 2cf9a09baeca59a644c381400f2543564095620d..6e4ff499c89a719b6f68988fe22fa9b4e7924647 100644
--- a/docs/questions/query-builder/expressions-list.md
+++ b/docs/questions/query-builder/expressions-list.md
@@ -767,13 +767,6 @@ Example: `Offset(Sum([Total]), -1)` would get the `Sum([Total])` value from the
 ## Limitations
 
 - [Aggregation expressions](#aggregations) can only be used in the **Summarize** section of the query builder.
-- Functions that return a boolean value, like [isempty](#isempty) or [contains](#contains), cannot be used to create a custom column. To create a custom column based on one of these functions, you must combine them with another function, like `case`.
-
-  For example, to create a new custom column that contains `true` if `[Title]` contain `'Wallet'`, you can use the custom expression
-
-  ```
-  case(contains([Title], 'Wallet'), true, false)
-  ```
 
 ## Database limitations
 
diff --git a/docs/releases.md b/docs/releases.md
index 9f835829be3be0e8046f03dd3b544ecd93deda90..930b6c99e90912238c96867f368831290ff0347b 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -19,6 +19,7 @@ To see what's new, check out all the [major release announcements](https://www.m
 
 ## Metabase Enterprise Edition releases
 
+- [v1.51.2](https://github.com/metabase/metabase/releases/tag/v1.51.2)
 - [v1.51.1](https://github.com/metabase/metabase/releases/tag/v1.51.1)
 - [v1.50.31](https://github.com/metabase/metabase/releases/tag/v1.50.31)
 - [v1.50.30](https://github.com/metabase/metabase/releases/tag/v1.50.30)
@@ -211,6 +212,7 @@ To see what's new, check out all the [major release announcements](https://www.m
 
 ## Metabase Open Source Edition releases
 
+- [v0.51.2](https://github.com/metabase/metabase/releases/tag/v0.51.2)
 - [v0.51.1](https://github.com/metabase/metabase/releases/tag/v0.51.1)
 - [v0.50.31](https://github.com/metabase/metabase/releases/tag/v0.50.31)
 - [v0.50.30](https://github.com/metabase/metabase/releases/tag/v0.50.30)
diff --git a/e2e/support/helpers/e2e-dashboard-helpers.ts b/e2e/support/helpers/e2e-dashboard-helpers.ts
index baf0b34866e6685ff3c1b8e8a44e33e3e91fc1de..4cd7ae0f23c32ef06b73b8fe911e83761293bc1a 100644
--- a/e2e/support/helpers/e2e-dashboard-helpers.ts
+++ b/e2e/support/helpers/e2e-dashboard-helpers.ts
@@ -88,12 +88,10 @@ export function saveDashboard({
   cy.button(buttonLabel).click();
 
   if (awaitRequest) {
-    cy.wait("@saveDashboardCards").then(() => {
-      cy.findByText(editBarText).should("not.exist");
-    });
-  } else {
-    cy.findByText(editBarText).should("not.exist");
+    cy.wait("@saveDashboardCards");
   }
+
+  cy.findByText(editBarText).should("not.exist");
   cy.wait(waitMs); // this is stupid but necessary to due to the dashboard resizing and detaching elements
 }
 
diff --git a/e2e/test/scenarios/custom-column/cc-boolean-functions.cy.spec.ts b/e2e/test/scenarios/custom-column/cc-boolean-functions.cy.spec.ts
index 977bd95c586b7b3474c7b46bde9c08c0b5b05215..fd926e4803e22671261bf30b2a538d12f302e3bc 100644
--- a/e2e/test/scenarios/custom-column/cc-boolean-functions.cy.spec.ts
+++ b/e2e/test/scenarios/custom-column/cc-boolean-functions.cy.spec.ts
@@ -4,6 +4,7 @@ import {
   type StructuredQuestionDetails,
   assertQueryBuilderRowCount,
   assertTableData,
+  changeSynchronousBatchUpdateSetting,
   createDashboard,
   createQuestion,
   editDashboard,
@@ -625,6 +626,17 @@ describe("scenarios > custom column > boolean functions", () => {
       );
     }
 
+    beforeEach(() => {
+      cy.signInAsAdmin();
+      changeSynchronousBatchUpdateSetting(true);
+      cy.signInAsNormalUser();
+    });
+
+    afterEach(() => {
+      cy.signInAsAdmin();
+      changeSynchronousBatchUpdateSetting(false);
+    });
+
     it("should be able setup an 'open question' click behavior", () => {
       createDashboardWithQuestion().then(dashboard =>
         visitDashboard(dashboard.id),
@@ -681,7 +693,9 @@ describe("scenarios > custom column > boolean functions", () => {
         .findByText(parameterDetails.name)
         .click();
       popover().findByText(expressionName).click();
+      cy.intercept("GET", "/api/dashboard/*").as("getDashboard");
       saveDashboard();
+      cy.wait("@getDashboard");
 
       cy.log("verify click behavior");
       getDashboardCard().within(() => {
diff --git a/e2e/test/scenarios/dashboard/dashboard-reproductions.cy.spec.js b/e2e/test/scenarios/dashboard/dashboard-reproductions.cy.spec.js
index 7f48ea40a206aee3c0dab2547f70d474c8606bb5..3923b30af665a3ebf70e6753eb7feea240a5d176 100644
--- a/e2e/test/scenarios/dashboard/dashboard-reproductions.cy.spec.js
+++ b/e2e/test/scenarios/dashboard/dashboard-reproductions.cy.spec.js
@@ -323,119 +323,120 @@ describe("issue 16559", () => {
     restore();
     cy.signInAsAdmin();
 
-    cy.createDashboard(dashboardDetails).then(
-      ({ body: { id: dashboardId } }) => {
-        visitDashboard(dashboardId);
-      },
-    );
-  });
-
-  it(
-    "should always show the most recent revision (metabase#16559)",
-    { tags: "@flaky" },
-    () => {
-      openDashboardInfoSidebar();
-      sidesheet().within(() => {
-        cy.findByRole("tab", { name: "History" }).click();
-        cy.log("Dashboard creation");
-        cy.findByTestId("dashboard-history-list")
-          .findAllByRole("listitem")
-          .eq(0)
-          .findByText("You created this.")
-          .should("be.visible");
-      });
-      closeDashboardInfoSidebar();
-
-      cy.log("Edit dashboard");
-      editDashboard();
-      openQuestionsSidebar();
-      sidebar().findByText("Orders, Count").click();
-      cy.button("Save").click();
-
-      openDashboardInfoSidebar();
-      sidesheet().within(() => {
-        cy.findByRole("tab", { name: "History" }).click();
-        cy.findByTestId("dashboard-history-list")
-          .findAllByRole("listitem")
-          .eq(0)
-          .findByText("You added a card.")
-          .should("be.visible");
-      });
-      closeDashboardInfoSidebar();
+    createDashboard(dashboardDetails).then(response => {
+      visitDashboard(response.body.id);
+    });
 
-      cy.log("Change dashboard name");
-      cy.findByTestId("dashboard-name-heading")
-        .click()
-        .type(" modified")
-        .blur();
+    cy.intercept("GET", "/api/collection/tree?*").as("getCollections");
+    cy.intercept("PUT", "/api/dashboard/*").as("saveDashboard");
+    cy.intercept("POST", "/api/card/*/query").as("cardQuery");
+  });
 
-      openDashboardInfoSidebar();
-      sidesheet().within(() => {
-        cy.findByRole("tab", { name: "History" }).click();
+  it("should always show the most recent revision (metabase#16559)", () => {
+    openDashboardInfoSidebar();
+    sidesheet().within(() => {
+      cy.findByRole("tab", { name: "History" }).click();
+      cy.log("Dashboard creation");
+      cy.findByTestId("dashboard-history-list")
+        .findAllByRole("listitem")
+        .eq(0)
+        .findByText("You created this.")
+        .should("be.visible");
+    });
+    closeDashboardInfoSidebar();
 
-        cy.findByTestId("dashboard-history-list")
-          .findAllByRole("listitem")
-          .eq(0)
-          .findByText(
-            'You renamed this Dashboard from "16559 Dashboard" to "16559 Dashboard modified".',
-          )
-          .should("be.visible");
+    cy.log("Edit dashboard");
+    editDashboard();
+    openQuestionsSidebar();
+    sidebar().findByText("Orders, Count").click();
+    cy.wait("@cardQuery");
+    cy.button("Save").click();
+    cy.wait("@saveDashboard");
+
+    openDashboardInfoSidebar();
+    sidesheet().within(() => {
+      cy.findByRole("tab", { name: "History" }).click();
+      cy.findByTestId("dashboard-history-list")
+        .findAllByRole("listitem")
+        .eq(0)
+        .findByText("You added a card.")
+        .should("be.visible");
+    });
+    closeDashboardInfoSidebar();
 
-        cy.log("Add description");
-        cy.findByRole("tab", { name: "Overview" }).click();
+    cy.log("Change dashboard name");
+    cy.findByTestId("dashboard-name-heading").click().type(" modified").blur();
+    cy.wait("@saveDashboard");
 
-        cy.findByPlaceholderText("Add description")
-          .click()
-          .type("16559 description")
-          .blur();
+    openDashboardInfoSidebar();
+    sidesheet().within(() => {
+      cy.findByRole("tab", { name: "History" }).click();
 
-        cy.findByRole("tab", { name: "History" }).click();
+      cy.findByTestId("dashboard-history-list")
+        .findAllByRole("listitem")
+        .eq(0)
+        .findByText(
+          'You renamed this Dashboard from "16559 Dashboard" to "16559 Dashboard modified".',
+        )
+        .should("be.visible");
 
-        cy.findByTestId("dashboard-history-list")
-          .findAllByRole("listitem")
-          .eq(0)
-          .findByText("You added a description.")
-          .should("be.visible");
+      cy.log("Add description");
+      cy.findByRole("tab", { name: "Overview" }).click();
 
-        cy.log("Toggle auto-apply filters");
-      });
-      closeDashboardInfoSidebar();
+      cy.findByPlaceholderText("Add description")
+        .click()
+        .type("16559 description")
+        .blur();
+      cy.wait("@saveDashboard");
 
-      openDashboardSettingsSidebar();
-      sidesheet().findByText("Auto-apply filters").click();
-      closeDashboardSettingsSidebar();
+      cy.findByRole("tab", { name: "History" }).click();
 
-      openDashboardInfoSidebar();
-      sidesheet().within(() => {
-        cy.findByRole("tab", { name: "History" }).click();
+      cy.findByTestId("dashboard-history-list")
+        .findAllByRole("listitem")
+        .eq(0)
+        .findByText("You added a description.")
+        .should("be.visible");
 
-        cy.findByTestId("dashboard-history-list")
-          .findAllByRole("listitem")
-          .eq(0)
-          .findByText("You set auto apply filters to false.")
-          .should("be.visible");
-      });
-      closeDashboardInfoSidebar();
-
-      cy.log("Move dashboard to another collection");
-      dashboardHeader().icon("ellipsis").click();
-      popover().findByText("Move").click();
-      entityPickerModal().within(() => {
-        cy.findByText("First collection").click();
-        cy.button("Move").click();
-      });
+      cy.log("Toggle auto-apply filters");
+    });
+    closeDashboardInfoSidebar();
+
+    openDashboardSettingsSidebar();
+    sidesheet().findByText("Auto-apply filters").click();
+    cy.wait("@saveDashboard");
+    closeDashboardSettingsSidebar();
+
+    openDashboardInfoSidebar();
+    sidesheet().within(() => {
+      cy.findByRole("tab", { name: "History" }).click();
+
+      cy.findByTestId("dashboard-history-list")
+        .findAllByRole("listitem")
+        .eq(0)
+        .findByText("You set auto apply filters to false.")
+        .should("be.visible");
+    });
+    closeDashboardInfoSidebar();
+
+    cy.log("Move dashboard to another collection");
+    dashboardHeader().icon("ellipsis").click();
+    popover().findByText("Move").click();
+    entityPickerModal().within(() => {
+      cy.findByText("First collection").click();
+      cy.button("Move").click();
+      cy.wait(["@saveDashboard", "@getCollections"]);
+    });
 
-      openDashboardInfoSidebar();
-      sidesheet().within(() => {
-        cy.findByRole("tab", { name: "History" }).click();
-        cy.findByTestId("dashboard-history-list")
-          .findAllByRole("listitem")
-          .eq(0)
-          .findByText("You moved this Dashboard to First collection.")
-          .should("be.visible");
-      });
-    },
-  );
+    openDashboardInfoSidebar();
+    sidesheet().within(() => {
+      cy.findByRole("tab", { name: "History" }).click();
+      cy.findByTestId("dashboard-history-list")
+        .findAllByRole("listitem")
+        .eq(0)
+        .findByText("You moved this Dashboard to First collection.")
+        .should("be.visible");
+    });
+  });
 });
 
 describe("issue 17879", () => {
diff --git a/frontend/src/metabase/lib/icon.ts b/frontend/src/metabase/lib/icon.ts
index 2c4919a9016583704b79868c78c078a7095bba99..7013d100547e9de903a7d29569cb9087b09b3060 100644
--- a/frontend/src/metabase/lib/icon.ts
+++ b/frontend/src/metabase/lib/icon.ts
@@ -9,7 +9,7 @@ import type {
   SearchModel,
 } from "metabase-types/api";
 
-type IconModel = SearchModel | CollectionItemModel | "schema";
+export type IconModel = SearchModel | CollectionItemModel | "schema";
 
 export type ObjectWithModel = {
   id?: unknown;
@@ -22,7 +22,7 @@ export type ObjectWithModel = {
   is_personal?: boolean;
 };
 
-const modelIconMap: Record<IconModel, IconName> = {
+export const modelIconMap: Record<IconModel, IconName> = {
   collection: "folder",
   database: "database",
   table: "table",
diff --git a/frontend/src/metabase/query_builder/components/view/ViewHeader/components/QuestionDataSource/utils.js b/frontend/src/metabase/query_builder/components/view/ViewHeader/components/QuestionDataSource/utils.js
index 81253ecbb4ba1e1da729e50fc8c778a7705b95b8..583d67926a1e6361cb30931bbf61b7258013f5cc 100644
--- a/frontend/src/metabase/query_builder/components/view/ViewHeader/components/QuestionDataSource/utils.js
+++ b/frontend/src/metabase/query_builder/components/view/ViewHeader/components/QuestionDataSource/utils.js
@@ -17,7 +17,12 @@ import { HeadBreadcrumbs } from "../HeaderBreadcrumbs";
 
 import { IconWrapper, TablesDivider } from "./QuestionDataSource.styled";
 
-export function getDataSourceParts({ question, subHead, isObjectDetail }) {
+export function getDataSourceParts({
+  question,
+  subHead,
+  isObjectDetail,
+  formatTableAsComponent = true,
+}) {
   if (!question) {
     return [];
   }
@@ -40,6 +45,7 @@ export function getDataSourceParts({ question, subHead, isObjectDetail }) {
       icon: !subHead ? "database" : undefined,
       name: database.displayName(),
       href: database.id >= 0 && Urls.browseDatabase(database),
+      model: "database",
     });
   }
 
@@ -50,6 +56,7 @@ export function getDataSourceParts({ question, subHead, isObjectDetail }) {
     const isBasedOnSavedQuestion = isVirtualCardId(table.id);
     if (!isBasedOnSavedQuestion) {
       parts.push({
+        model: "schema",
         name: table.schema_name,
         href: database.id >= 0 && Urls.browseSchema(table),
       });
@@ -82,14 +89,22 @@ export function getDataSourceParts({ question, subHead, isObjectDetail }) {
         }),
     ].filter(isNotNull);
 
-    parts.push(
+    const part = formatTableAsComponent ? (
       <QuestionTableBadges
         tables={allTables}
         subHead={subHead}
         hasLink={hasTableLink}
         isLast={!isObjectDetail}
-      />,
+      />
+    ) : (
+      {
+        name: table.displayName(),
+        href: hasTableLink ? getTableURL(table) : "",
+        model: table.type ?? "table",
+      }
     );
+
+    parts.push(part);
   }
 
   return parts.filter(part => isValidElement(part) || part.name || part.icon);
diff --git a/frontend/src/metabase/query_builder/components/view/ViewHeader/components/QuestionDataSource/utils.unit.spec.ts b/frontend/src/metabase/query_builder/components/view/ViewHeader/components/QuestionDataSource/utils.unit.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e57594e4090a1d7809403dbe643481f653d9b916
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/ViewHeader/components/QuestionDataSource/utils.unit.spec.ts
@@ -0,0 +1,88 @@
+import { isValidElement } from "react";
+
+import { createMockMetadata } from "__support__/metadata";
+import Question from "metabase-lib/v1/Question";
+import type { Card } from "metabase-types/api";
+import {
+  createMockCard,
+  createMockDatabase,
+  createMockTable,
+} from "metabase-types/api/mocks";
+import { createSampleDatabase } from "metabase-types/api/mocks/presets";
+
+import { getDataSourceParts } from "./utils";
+
+const MULTI_SCHEMA_DB_ID = 2;
+const MULTI_SCHEMA_TABLE1_ID = 100;
+const MULTI_SCHEMA_TABLE2_ID = 101;
+
+function getMetadata() {
+  return createMockMetadata({
+    databases: [
+      createSampleDatabase(),
+      createMockDatabase({
+        id: MULTI_SCHEMA_DB_ID,
+        tables: [
+          createMockTable({
+            id: MULTI_SCHEMA_TABLE1_ID,
+            db_id: MULTI_SCHEMA_DB_ID,
+            schema: "first_schema",
+          }),
+          createMockTable({
+            id: MULTI_SCHEMA_TABLE2_ID,
+            db_id: MULTI_SCHEMA_DB_ID,
+            schema: "second_schema",
+          }),
+        ],
+      }),
+    ],
+  });
+}
+
+const createMockQuestion = (opts?: Partial<Card>) =>
+  new Question(createMockCard(opts), getMetadata());
+
+/** These tests cover new logic in the getDataSourceParts utility that is not covered in QuestionDataSource.unit.spec.js */
+describe("getDataSourceParts", () => {
+  afterEach(() => {
+    jest.restoreAllMocks();
+  });
+  it("returns an array of Records if formatTableAsComponent is false", () => {
+    const parts = getDataSourceParts({
+      question: createMockQuestion(),
+      subHead: false,
+      isObjectDetail: true,
+      formatTableAsComponent: false,
+    });
+    expect(parts).toHaveLength(2);
+    const partsArray = parts as any[];
+    expect(partsArray[0]).toEqual({
+      icon: "database",
+      name: "Sample Database",
+      href: "/browse/databases/1-sample-database",
+      model: "database",
+    });
+    expect(partsArray[1].name).toEqual("Products");
+    expect(partsArray[1].model).toEqual("table");
+    expect(partsArray[1].href).toMatch(/^\/question#[a-zA-Z0-9]{50}/);
+    expect(Object.keys(partsArray[1])).toHaveLength(3);
+  });
+
+  it("returns an array with the table formatted as a component if formatTableAsComponent is true", () => {
+    const parts = getDataSourceParts({
+      question: createMockQuestion(),
+      subHead: false,
+      isObjectDetail: true,
+      formatTableAsComponent: true,
+    });
+    expect(parts).toHaveLength(2);
+    const partsArray = parts as any[];
+    expect(partsArray[0]).toEqual({
+      icon: "database",
+      name: "Sample Database",
+      href: "/browse/databases/1-sample-database",
+      model: "database",
+    });
+    expect(isValidElement(partsArray[1])).toBe(true);
+  });
+});
diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/QuestionDetails.tsx b/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/QuestionDetails.tsx
index fca6c9a29e479b32cb6b9de1101d9abe960a4bcc..be1534814f9326ea667f983b2b5b33a94a9a8907 100644
--- a/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/QuestionDetails.tsx
+++ b/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/QuestionDetails.tsx
@@ -2,22 +2,19 @@ import cx from "classnames";
 import { useState } from "react";
 import { c, t } from "ttag";
 
-import { getTableUrl } from "metabase/browse/containers/TableBrowser/TableBrowser";
 import { getCollectionName } from "metabase/collections/utils";
 import { SidesheetCardSection } from "metabase/common/components/Sidesheet";
 import DateTime from "metabase/components/DateTime";
 import Link from "metabase/core/components/Link";
 import Styles from "metabase/css/core/index.css";
-import { useSelector } from "metabase/lib/redux";
 import * as Urls from "metabase/lib/urls";
 import { getUserName } from "metabase/lib/user";
-import { getMetadata } from "metabase/selectors/metadata";
 import { QuestionPublicLinkPopover } from "metabase/sharing/components/PublicLinkPopover";
 import { Box, Flex, FixedSizeIcon as Icon, Text } from "metabase/ui";
 import type Question from "metabase-lib/v1/Question";
-import type { Database } from "metabase-types/api";
 
 import SidebarStyles from "./QuestionInfoSidebar.module.css";
+import { QuestionSources } from "./components/QuestionSources";
 
 export const QuestionDetails = ({ question }: { question: Question }) => {
   const lastEditInfo = question.lastEditInfo();
@@ -74,52 +71,11 @@ export const QuestionDetails = ({ question }: { question: Question }) => {
         </Flex>
       </SidesheetCardSection>
       <SharingDisplay question={question} />
-      <SourceDisplay question={question} />
+      <QuestionSources question={question} />
     </>
   );
 };
 
-function SourceDisplay({ question }: { question: Question }) {
-  const sourceInfo = question.legacyQueryTable();
-  const metadata = useSelector(getMetadata);
-
-  if (!sourceInfo) {
-    return null;
-  }
-
-  const model = String(sourceInfo.id).includes("card__") ? "card" : "table";
-
-  const sourceUrl =
-    model === "card"
-      ? Urls.browseDatabase(sourceInfo.db as Database)
-      : getTableUrl(sourceInfo, metadata);
-
-  return (
-    <SidesheetCardSection title={t`Based on`}>
-      <Flex gap="sm" align="center">
-        {sourceInfo.db && (
-          <>
-            <Text>
-              <Link
-                to={`/browse/databases/${sourceInfo.db.id}`}
-                variant="brand"
-              >
-                {sourceInfo.db.name}
-              </Link>
-            </Text>
-            {"/"}
-          </>
-        )}
-        <Text>
-          <Link to={sourceUrl} variant="brand">
-            {sourceInfo?.display_name}
-          </Link>
-        </Text>
-      </Flex>
-    </SidesheetCardSection>
-  );
-}
-
 function SharingDisplay({ question }: { question: Question }) {
   const publicUUID = question.publicUUID();
   const embeddingEnabled = question._card.enable_embedding;
diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/components/QuestionSources.tsx b/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/components/QuestionSources.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7fa2bba3cc6e124639e32a663b56f15d165a56cf
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/components/QuestionSources.tsx
@@ -0,0 +1,56 @@
+import { Fragment, useMemo } from "react";
+import { c } from "ttag";
+
+import { SidesheetCardSection } from "metabase/common/components/Sidesheet";
+import Link from "metabase/core/components/Link";
+import { Flex, FixedSizeIcon as Icon } from "metabase/ui";
+import type Question from "metabase-lib/v1/Question";
+
+import { getDataSourceParts } from "../../../ViewHeader/components/QuestionDataSource/utils";
+
+import type { QuestionSource } from "./types";
+import { getIconPropsForSource } from "./utils";
+
+export const QuestionSources = ({ question }: { question: Question }) => {
+  const sources = getDataSourceParts({
+    question,
+    subHead: false,
+    isObjectDetail: true,
+    formatTableAsComponent: false,
+  }) as unknown as QuestionSource[];
+
+  const sourcesWithIcons: QuestionSource[] = useMemo(() => {
+    return sources.map(source => ({
+      ...source,
+      iconProps: getIconPropsForSource(source),
+    }));
+  }, [sources]);
+
+  if (!sources.length) {
+    return null;
+  }
+
+  const title = c(
+    "This is a heading that appears above the names of the database, table, and/or question that a question is based on -- the 'sources' for the question. Feel free to translate this heading as though it said 'Based on these sources', if you think that would make more sense in your language.",
+  ).t`Based on`;
+
+  return (
+    <SidesheetCardSection title={title}>
+      <Flex gap="sm" align="flex-start">
+        {sourcesWithIcons.map(({ href, name, iconProps }, index) => (
+          <Fragment key={`${href}-${name}`}>
+            <Link to={href} variant="brand">
+              <Flex gap="sm" lh="1.25rem" maw="20rem">
+                {iconProps ? (
+                  <Icon mt={2} c="text-dark" {...iconProps} />
+                ) : null}
+                {name}
+              </Flex>
+            </Link>
+            {index < sources.length - 1 && <Flex lh="1.25rem">{"/"}</Flex>}
+          </Fragment>
+        ))}
+      </Flex>
+    </SidesheetCardSection>
+  );
+};
diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/components/QuestionSources.unit.spec.tsx b/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/components/QuestionSources.unit.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d12e56ed2bf0549c39171d8f665051a5fefbf803
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/components/QuestionSources.unit.spec.tsx
@@ -0,0 +1,145 @@
+import { Route } from "react-router";
+import _ from "underscore";
+
+import { mockSettings } from "__support__/settings";
+import { createMockEntitiesState } from "__support__/store";
+import { renderWithProviders, screen, within } from "__support__/ui";
+import { modelIconMap } from "metabase/lib/icon";
+import { checkNotNull } from "metabase/lib/types";
+import { getMetadata } from "metabase/selectors/metadata";
+import { convertSavedQuestionToVirtualTable } from "metabase-lib/v1/metadata/utils/saved-questions";
+import type { Card, NormalizedTable } from "metabase-types/api";
+import { createMockCard, createMockSettings } from "metabase-types/api/mocks";
+import { createSampleDatabase } from "metabase-types/api/mocks/presets";
+import { createMockState } from "metabase-types/store/mocks";
+
+import { QuestionSources } from "./QuestionSources";
+
+interface SetupOpts {
+  card?: Card;
+  sourceCard?: Card;
+}
+
+const setup = async ({
+  card = createMockCard(),
+  sourceCard,
+}: SetupOpts = {}) => {
+  const state = createMockState({
+    settings: mockSettings(createMockSettings()),
+    entities: createMockEntitiesState({
+      databases: [createSampleDatabase()],
+      questions: _.compact([card, sourceCard]),
+    }),
+  });
+
+  // 😫 all this is necessary to test a card as a question source
+  if (sourceCard) {
+    const virtualTable = convertSavedQuestionToVirtualTable(sourceCard);
+
+    state.entities = {
+      ...state.entities,
+      tables: {
+        ...(state.entities.tables as Record<number, NormalizedTable>),
+        [virtualTable.id]: virtualTable,
+      },
+      databases: {
+        [state.entities.databases[1].id]: {
+          ...state.entities.databases[1],
+          tables: [
+            ...(state.entities.databases[1].tables ?? []),
+            virtualTable.id,
+          ],
+        },
+      },
+    };
+  }
+
+  const metadata = getMetadata(state);
+  const question = checkNotNull(metadata.question(card.id));
+
+  return renderWithProviders(
+    <Route
+      path="/"
+      component={() => <QuestionSources question={question} />}
+    />,
+    {
+      withRouter: true,
+    },
+  );
+};
+
+describe("QuestionSources", () => {
+  it("should show table source information", async () => {
+    const card = createMockCard({
+      name: "Question",
+    });
+    setup({ card });
+    const databaseLink = await screen.findByRole("link", {
+      name: /Sample Database/i,
+    });
+    expect(
+      await within(databaseLink).findByLabelText("database icon"),
+    ).toBeInTheDocument();
+    expect(databaseLink).toHaveAttribute(
+      "href",
+      "/browse/databases/1-sample-database",
+    );
+    expect(screen.getByText("/")).toBeInTheDocument();
+    const tableLink = await screen.findByRole("link", { name: /Products/i });
+    expect(tableLink).toBeInTheDocument();
+    expect(
+      await within(tableLink).findByLabelText(`table icon`),
+    ).toBeInTheDocument();
+    expect(tableLink).toHaveAttribute(
+      "href",
+      expect.stringMatching(/^\/question#[a-zA-Z0-9]{20}/),
+    );
+  });
+
+  it("should show card source information", async () => {
+    const card = createMockCard({
+      name: "My Question",
+      dataset_query: {
+        type: "query",
+        database: 1,
+        query: {
+          "source-table": "card__2",
+        },
+      },
+    });
+
+    const sourceCard = createMockCard({
+      name: "My Source Question",
+      id: 2,
+    });
+
+    await setup({ card, sourceCard });
+
+    const databaseLink = await screen.findByRole("link", {
+      name: /Sample Database/i,
+    });
+
+    expect(
+      await within(databaseLink).findByLabelText("database icon"),
+    ).toBeInTheDocument();
+    expect(databaseLink).toHaveAttribute(
+      "href",
+      "/browse/databases/1-sample-database",
+    );
+
+    expect(screen.getByText("/")).toBeInTheDocument();
+
+    const questionLink = await screen.findByRole("link", {
+      name: /My Source Question/i,
+    });
+    expect(
+      await within(questionLink).findByLabelText(
+        `${modelIconMap["card"]} icon`,
+      ),
+    ).toBeInTheDocument();
+    expect(questionLink).toHaveAttribute(
+      "href",
+      "/question/2-my-source-question",
+    );
+  });
+});
diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/components/types.ts b/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/components/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3f42bc97578c4810cd874b357f4b8841e1dda8c1
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/components/types.ts
@@ -0,0 +1,8 @@
+import type { IconData } from "metabase/lib/icon";
+
+export interface QuestionSource {
+  href: string;
+  name: string;
+  model?: string;
+  iconProps?: IconData;
+}
diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/components/utils.tsx b/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/components/utils.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..dd3b0538ca0d7fe2e4486486b135441387ab2335
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/view/sidebars/QuestionInfoSidebar/components/utils.tsx
@@ -0,0 +1,21 @@
+import { match } from "ts-pattern";
+
+import { type IconData, type IconModel, getIcon } from "metabase/lib/icon";
+
+import type { QuestionSource } from "./types";
+
+export const getIconPropsForSource = (
+  source: QuestionSource,
+): IconData | undefined => {
+  const iconModel: IconModel | undefined = match(source.model)
+    .with("question", () => "card" as const)
+    .with("model", () => "dataset" as const)
+    .with("database", () => "database" as const)
+
+    .with("metric", () => "metric" as const)
+    .with("schema", () => undefined)
+    .otherwise(() => "table" as const);
+
+  const iconProps = iconModel ? getIcon({ model: iconModel }) : undefined;
+  return iconProps;
+};
diff --git a/modules/drivers/databricks/src/metabase/driver/databricks.clj b/modules/drivers/databricks/src/metabase/driver/databricks.clj
index 2f55d00a4ac7a9aa1a829c7083a66d5d027b9748..f5e2e0bff493030a2084ff28aa25d01d596c34e9 100644
--- a/modules/drivers/databricks/src/metabase/driver/databricks.clj
+++ b/modules/drivers/databricks/src/metabase/driver/databricks.clj
@@ -3,6 +3,7 @@
    [clojure.string :as str]
    [honey.sql :as sql]
    [java-time.api :as t]
+   [metabase.config :as config]
    [metabase.driver :as driver]
    [metabase.driver.hive-like :as driver.hive-like]
    [metabase.driver.sql-jdbc.connection :as sql-jdbc.conn]
@@ -205,6 +206,7 @@
     :HttpPath       http-path
     :uid            "token"
     :pwd            token
+    :UserAgentEntry (format "Metabase/%s" (:tag config/mb-version-info))
     :UseNativeQuery 1}
    ;; Following is used just for tests. See the [[metabase.driver.sql-jdbc.connection-test/perturb-db-details]]
    ;; and test that is using the function.
diff --git a/modules/drivers/databricks/test/metabase/driver/databricks_test.clj b/modules/drivers/databricks/test/metabase/driver/databricks_test.clj
index ffcae30e1ff8a54d6c7fc01faaecf9c3aed0ee13..c80fb5f47877f46ec5d1ded2fda47b38d9944d01 100644
--- a/modules/drivers/databricks/test/metabase/driver/databricks_test.clj
+++ b/modules/drivers/databricks/test/metabase/driver/databricks_test.clj
@@ -1,7 +1,9 @@
 (ns ^:mb/driver-tests metabase.driver.databricks-test
   (:require
+   [clojure.java.jdbc :as jdbc]
    [clojure.test :refer :all]
    [java-time.api :as t]
+   [metabase.config :as config]
    [metabase.driver :as driver]
    [metabase.driver.databricks :as databricks]
    [metabase.driver.sql-jdbc.connection :as sql-jdbc.conn]
@@ -242,6 +244,17 @@
 (deftest additional-options-test
   (mt/test-driver
     :databricks
+    (testing "Connections with UserAgentEntry"
+      (sql-jdbc.conn/with-connection-spec-for-testing-connection
+       [spec [:databricks (:details (mt/db))]]
+        (is (= [{:a 1}] (jdbc/query spec ["select 1 as a"]))))
+      (with-redefs [config/mb-version-info {:tag "invalid agent"}]
+        (sql-jdbc.conn/with-connection-spec-for-testing-connection
+         [spec [:databricks (:details (mt/db))]]
+          (is (thrown-with-msg?
+               Exception
+               #"Incorrect format for User-Agent entry"
+               (jdbc/query spec ["select 1 as a"]))))))
     (testing "Additional options are added to :subname key of generated spec"
       (is (re-find #";IgnoreTransactions=0$"
                    (->> {:http-path "p/a/t/h",
diff --git a/src/metabase/models/card.clj b/src/metabase/models/card.clj
index 48deda5e0d5e71622f63045d317601f7fb15820b..41bb26a7c43d500278731536cb208337cf09f083 100644
--- a/src/metabase/models/card.clj
+++ b/src/metabase/models/card.clj
@@ -783,6 +783,92 @@
     :query_type ;; these first three may not even be changeable
     :dataset_query})
 
+(defn- breakout-->identifier->refs
+  "Generate mapping of of _ref identifier_ -> #{_ref..._}.
+
+  _ref identifier_ is a vector of first 2 elements of ref, eg. [:expression \"xix\"] or [:field 10]"
+  [breakouts]
+  (-> (group-by #(subvec % 0 2) breakouts)
+      (update-vals set)))
+
+(defn- action-for-identifier+refs
+  "Generate _action_ for combination of args.
+
+  _Action_ is to be performed on _parameter mapping_ of a _dashcard_. For more info see
+  the [[update-associated-parameters!]]'s docstring.
+
+  _Action_ has a form of [<action> & args]."
+  [after--identifier->refs identifier before--refs]
+  (let [after--refs (get after--identifier->refs identifier #{})]
+    (when (and (= 1 (count before--refs) (count after--refs))
+               (not= before--refs after--refs))
+      [:update (first after--refs)])))
+
+(defn- breakouts-->identifier->action
+  "Generate mapping of _identifier_ -> _action_.
+
+  _identifier_ is is a vector of first 2 elements of ref, eg. [:expression \"xix\"] or [:field 10]. Action is generated
+  in [[action-for-identifier+refs]] and performed later in [[update-mapping]]."
+  [breakout-before-update breakout-after-update]
+  (let [before--identifier->refs (breakout-->identifier->refs breakout-before-update)
+        after--identifier->refs  (breakout-->identifier->refs breakout-after-update)]
+    ;; Remove no-ops to avoid redundant db calls in [[update-associated-parameters!]].
+    (->> before--identifier->refs
+         (m/map-kv-vals #(action-for-identifier+refs after--identifier->refs %1 %2))
+         (m/filter-vals some?)
+         not-empty)))
+
+(defn eligible-mapping?
+  "Decide whether parameter mapping has strucuture so it can be updated presumably using [[update-mapping]]."
+  [{[dim [ref-kind]] :target :as _mapping}]
+  (and (= dim :dimension)
+       (#{:field :expression} ref-kind)))
+
+(defn- update-mapping
+  "Return modifed mapping according to action."
+  [identifier->action {[_dim ref] :target :as mapping}]
+  (let [identifier (subvec ref 0 2)
+        [action arg] (get identifier->action identifier)]
+    (case action
+      :update (assoc-in mapping [:target 1] arg)
+      mapping)))
+
+(defn- updates-for-dashcards
+  [identifier->action dashcards]
+  (not-empty (for [{:keys [id parameter_mappings]} dashcards
+                   :let [updated (into [] (map #(if (eligible-mapping? %)
+                                                  (update-mapping identifier->action %)
+                                                  %))
+                                       parameter_mappings)]
+                   :when (not= parameter_mappings updated)]
+               [id {:parameter_mappings updated}])))
+
+(defn- update-associated-parameters!
+  "Update _parameter mappings_ of _dashcards_ that target modified _card_, to reflect the modification.
+
+  This function handles only modifications to breakout.
+
+  Context. Card can have multiple multiple breakout elements referencing same field or expression, having different
+  _temporal unit_. Those refs can be targeted by dashboard _temporal unit parameter_. If refs change, and card is saved,
+  _parameter mappings_ have to be updated to target new, modified refs. This function takes care of that.
+
+  First mappings of _identifier_ -> _action_ are generated. _identifier_ is described
+  eg. in [[breakouts-->identifier->action]] docstring. Then, dashcards are fetched and updates are generated
+  by [[updates-for-dashcards]]. Updates are then executed."
+  [card-before card-after]
+  (let [card->breakout     #(-> % :dataset_query mbql.normalize/normalize :query :breakout)
+        breakout-before    (card->breakout card-before)
+        breakout-after     (card->breakout card-after)]
+    (when-some [identifier->action (breakouts-->identifier->action breakout-before breakout-after)]
+      (let [dashcards          (t2/select :model/DashboardCard :card_id (some :id [card-after card-before]))
+            updates            (updates-for-dashcards identifier->action dashcards)]
+        ;; Beware. This can have negative impact on card update performance as queries are fired in sequence. I'm not
+        ;; aware of more reasonable way.
+        (when (seq updates)
+          (t2/with-transaction [_conn]
+            (doseq [[id update] updates]
+              (t2/update! :model/DashboardCard :id id update))))))))
+
 (defn update-card!
   "Update a Card. Metadata is fetched asynchronously. If it is ready before [[metadata-sync-wait-ms]] elapses it will be
   included, otherwise the metadata will be saved to the database asynchronously."
@@ -808,7 +894,15 @@
                                     :present #{:collection_id :collection_position :description :cache_ttl :archived_directly}
                                     :non-nil #{:dataset_query :display :name :visualization_settings :archived
                                                :enable_embedding :type :parameters :parameter_mappings :embedding_params
-                                               :result_metadata :collection_preview :verified-result-metadata?})))
+                                               :result_metadata :collection_preview :verified-result-metadata?}))
+    ;; ok, now update dependent dashcard parameters
+    (try
+      (update-associated-parameters! card-before-update card-updates)
+      (catch Throwable e
+        (log/error "Update of dependent card parameters failed!")
+        (log/debug e
+                   "`card-before-update`:" (pr-str card-before-update)
+                   "`card-updates`:" (pr-str card-updates)))))
   ;; 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)
diff --git a/src/metabase/query_processor/pivot.clj b/src/metabase/query_processor/pivot.clj
index ecbca94d08165e47b158348344797add0bde2711..86c736a1968b097482e5a6c39200355c9bd42542 100644
--- a/src/metabase/query_processor/pivot.clj
+++ b/src/metabase/query_processor/pivot.clj
@@ -45,8 +45,15 @@
 (mr/def ::num-breakouts ::lib.schema.common/int-greater-than-or-equal-to-zero)
 (mr/def ::index         ::lib.schema.common/int-greater-than-or-equal-to-zero)
 
-(mr/def ::pivot-rows [:sequential ::index])
-(mr/def ::pivot-cols [:sequential ::index])
+(mr/def ::pivot-rows     [:sequential ::index])
+(mr/def ::pivot-cols     [:sequential ::index])
+(mr/def ::pivot-measures [:sequential ::index])
+
+(mr/def ::pivot-opts [:maybe
+                      [:map
+                       [:pivot-rows     {:optional true} [:maybe ::pivot-rows]]
+                       [:pivot-cols     {:optional true} [:maybe ::pivot-cols]]
+                       [:pivot-measures {:optional true} [:maybe ::pivot-measures]]]])
 
 (mu/defn- group-bitmask :- ::bitmask
   "Come up with a display name given a combination of breakout `indexes` e.g.
@@ -173,9 +180,7 @@
 (mu/defn- generate-queries :- [:sequential ::lib.schema/query]
   "Generate the additional queries to perform a generic pivot table"
   [query                                               :- ::lib.schema/query
-   {:keys [pivot-rows pivot-cols], :as _pivot-options} :- [:map
-                                                           [:pivot-rows {:optional true} [:maybe ::pivot-rows]]
-                                                           [:pivot-cols {:optional true} [:maybe ::pivot-cols]]]]
+   {:keys [pivot-rows pivot-cols], :as _pivot-options} :- ::pivot-opts]
   (try
     (let [all-breakouts (lib/breakouts query)
           all-queries   (for [breakout-indexes (u/prog1 (breakout-combinations (count all-breakouts) pivot-rows pivot-cols)
@@ -342,48 +347,45 @@
               qp.pipeline/*reduce*  (or reduce qp.pipeline/*reduce*)]
       (qp/process-query first-query rff))))
 
-(mu/defn- pivot-options :- [:map
-                            [:pivot-rows [:maybe [:sequential [:int {:min 0}]]]]
-                            [:pivot-cols [:maybe [:sequential [:int {:min 0}]]]]]
+(mu/defn- pivot-options :- ::pivot-opts
   "Given a pivot table query and a card ID, looks at the `pivot_table.column_split` key in the card's visualization
   settings and generates pivot-rows and pivot-cols to use for generating subqueries."
   [query        :- [:map
                     [:database ::lib.schema.id/database]]
    viz-settings :- [:maybe :map]]
-  (let [column-split         (:pivot_table.column_split viz-settings)
-        column-split-rows    (seq (:rows column-split))
-        column-split-columns (seq (:columns column-split))
-        index-in-breakouts   (when (or column-split-rows
-                                       column-split-columns)
-                               (let [metadata-provider (or (:lib/metadata query)
-                                                           (lib.metadata.jvm/application-database-metadata-provider (:database query)))
-                                     mlv2-query        (lib/query metadata-provider query)
-                                     breakouts         (into []
-                                                             (map-indexed (fn [i col]
-                                                                            (cond-> col
-                                                                              true                         (assoc ::i i)
-                                                                              ;; if the col has a card-id, we swap the :lib/source to say source/card
-                                                                              ;; this allows `lib/find-matching-column` to properly match a column that has a join-alias
-                                                                              ;; but whose source is a model
-                                                                              (contains? col :lib/card-id) (assoc :lib/source :source/card))))
-                                                             (lib/breakouts-metadata mlv2-query))]
-                                 (fn [legacy-ref]
-                                   (try
-                                     (::i (lib.equality/find-column-for-legacy-ref
-                                           mlv2-query
-                                           -1
-                                           legacy-ref
-                                           breakouts))
-                                     (catch Throwable e
-                                       (log/errorf e "Error finding matching column for ref %s" (pr-str legacy-ref))
-                                       nil)))))
-
-        pivot-rows (when column-split-rows
-                     (into [] (keep index-in-breakouts) column-split-rows))
-        pivot-cols (when column-split-columns
-                     (into [] (keep index-in-breakouts) column-split-columns))]
-    {:pivot-rows pivot-rows
-     :pivot-cols pivot-cols}))
+  (let [{:keys [rows columns values]} (:pivot_table.column_split viz-settings)
+        metadata-provider             (or (:lib/metadata query)
+                                          (lib.metadata.jvm/application-database-metadata-provider (:database query)))
+        mlv2-query                    (lib/query metadata-provider query)
+        breakouts                     (into []
+                                            (map-indexed (fn [i col]
+                                                           (cond-> col
+                                                             true                         (assoc ::idx i)
+                                                             ;; if the col has a card-id, we swap the :lib/source to say
+                                                             ;; source/card this allows `lib/find-matching-column` to properly
+                                                             ;; match a column that has a join-alias but whose source is a
+                                                             ;; model
+                                                             (contains? col :lib/card-id) (assoc :lib/source :source/card))))
+                                            (concat (lib/breakouts-metadata mlv2-query)
+                                                    (lib/aggregations-metadata mlv2-query)))
+        index-in-breakouts            (fn index-in-breakouts [legacy-ref]
+                                        (try
+                                          (::idx (lib.equality/find-column-for-legacy-ref
+                                                  mlv2-query
+                                                  -1
+                                                  legacy-ref
+                                                  breakouts))
+                                          (catch Throwable e
+                                            (log/errorf e "Error finding matching column for ref %s" (pr-str legacy-ref))
+                                            nil)))
+        process-refs                  (fn process-refs [refs]
+                                        (when (seq refs)
+                                          (into [] (keep index-in-breakouts) refs)))
+        pivot-opts                    {:pivot-rows     (process-refs rows)
+                                       :pivot-cols     (process-refs columns)
+                                       :pivot-measures (process-refs values)}]
+    (when (some some? (vals pivot-opts))
+      pivot-opts)))
 
 (mu/defn- column-mapping-for-subquery :- ::pivot-column-mapping
   [num-canonical-cols            :- ::lib.schema.common/int-greater-than-or-equal-to-zero
@@ -529,10 +531,11 @@
      (qp.setup/with-qp-setup [query query]
        (let [rff               (or rff qp.reducible/default-rff)
              query             (lib/query (qp.store/metadata-provider) query)
-             pivot-options     (or
-                                (not-empty (select-keys query [:pivot-rows :pivot-cols]))
-                                (pivot-options query (get-in query [:info :visualization-settings])))
-             query             (assoc-in query [:middleware :pivot-options] pivot-options)
-             all-queries       (generate-queries query pivot-options)
+             pivot-opts        (or
+                                (pivot-options query (get query :viz-settings))
+                                (pivot-options query (get-in query [:info :visualization-settings]))
+                                (not-empty (select-keys query [:pivot-rows :pivot-cols :pivot-measures])))
+             query             (assoc-in query [:middleware :pivot-options] pivot-opts)
+             all-queries       (generate-queries query pivot-opts)
              column-mapping-fn (make-column-mapping-fn query)]
          (process-multiple-queries all-queries rff column-mapping-fn))))))
diff --git a/src/metabase/query_processor/pivot/postprocess.clj b/src/metabase/query_processor/pivot/postprocess.clj
index 7b277a2700147dfd05ee577a4d80133c059c2c13..a70414848ed9fe041381cc4b3efacefd56743a56 100644
--- a/src/metabase/query_processor/pivot/postprocess.clj
+++ b/src/metabase/query_processor/pivot/postprocess.clj
@@ -62,10 +62,19 @@
 
 (mu/defn add-pivot-measures :- ::pivot-spec
   "Given a pivot-spec map without the `:pivot-measures` key, determine what key(s) the measures will be and assoc that value into `:pivot-measures`."
-  [pivot-spec :- ::pivot-spec]
-  (-> pivot-spec
-      (assoc :pivot-measures (pivot-measures pivot-spec))
-      (assoc :pivot-grouping (pivot-grouping-key (:column-titles pivot-spec)))))
+  [{measure-indices :pivot-measures :as pivot-spec} :- ::pivot-spec]
+  (let [pivot-grouping-key (pivot-grouping-key (:column-titles pivot-spec))]
+    (cond-> pivot-spec
+      ;; if pivot-measures don't already exist (from the pivot qp), we add them ourselves, assuming lowest ID -> highest ID sort order
+      (not (seq measure-indices)) (assoc :pivot-measures (pivot-measures pivot-spec))
+      ;; otherwise, we modify indices to skip over whatever the pivot-grouping idx is, so we pull the correct values per row
+      (seq measure-indices)       (update :pivot-measures (fn [indices]
+                                                            (mapv (fn [idx]
+                                                                    (if (>= idx pivot-grouping-key)
+                                                                      (inc idx)
+                                                                      idx))
+                                                                  indices)))
+      true                       (assoc :pivot-grouping pivot-grouping-key))))
 
 (mu/defn add-totals-settings :- ::pivot-spec
   "Given a pivot-spec map and `viz-settings`, add the `:row-totals?` and `:col-totals?` keys."
diff --git a/src/metabase/query_processor/streaming/csv.clj b/src/metabase/query_processor/streaming/csv.clj
index 99b2d0173e598b79779b91115482799c3cf34e35..ab1a6529d3d133928a66352d3688e92ff52bf9db 100644
--- a/src/metabase/query_processor/streaming/csv.clj
+++ b/src/metabase/query_processor/streaming/csv.clj
@@ -78,15 +78,14 @@
       (begin! [_ {{:keys [ordered-cols results_timezone format-rows? pivot-export-options pivot?]
                    :or   {format-rows? true
                           pivot?       false}} :data} viz-settings]
-        (let [opts               (when (and pivot? pivot-export-options)
+        (let [col-names          (vec (common/column-titles ordered-cols (::mb.viz/column-settings viz-settings) format-rows?))
+              opts               (when (and pivot? pivot-export-options)
                                    (-> (merge {:pivot-rows []
                                                :pivot-cols []}
                                               pivot-export-options)
-                                       (assoc :column-titles (mapv :display_name ordered-cols))
+                                       (assoc :column-titles col-names)
                                        (qp.pivot.postprocess/add-totals-settings viz-settings)
                                        qp.pivot.postprocess/add-pivot-measures))
-              ;; col-names are created later when exporting a pivot table, so only create them if there are no pivot options
-              col-names          (when-not opts (common/column-titles ordered-cols (::mb.viz/column-settings viz-settings) format-rows?))
               pivot-grouping-key (qp.pivot.postprocess/pivot-grouping-key col-names)]
 
           ;; initialize the pivot-data
@@ -101,7 +100,7 @@
                    (mapv #(formatter/create-formatter results_timezone % viz-settings format-rows?) ordered-cols))
 
           ;; write the column names for non-pivot tables
-          (when col-names
+          (when (not opts)
             (let [header (m/remove-nth (or pivot-grouping-key (inc (count col-names))) col-names)]
               (write-csv writer [header])
               (.flush writer)))))
diff --git a/src/metabase/query_processor/streaming/json.clj b/src/metabase/query_processor/streaming/json.clj
index 2f851442d98bd16b36bb520a8d94796c6abe1a7f..05cf3de2c42422b0db0dd77dd377c846bfe4dace 100644
--- a/src/metabase/query_processor/streaming/json.clj
+++ b/src/metabase/query_processor/streaming/json.clj
@@ -41,8 +41,6 @@
     (reify qp.si/StreamingResultsWriter
       (begin! [_ {{:keys [ordered-cols results_timezone format-rows?]
                    :or   {format-rows? true}} :data} viz-settings]
-        ;; TODO -- wouldn't it make more sense if the JSON downloads used `:name` preferentially? Seeing how JSON is
-        ;; probably going to be parsed programmatically
         (let [cols           (common/column-titles ordered-cols (::mb.viz/column-settings viz-settings) format-rows?)
               pivot-grouping (qp.pivot.postprocess/pivot-grouping-key cols)]
           (when pivot-grouping (vreset! pivot-grouping-idx pivot-grouping))
diff --git a/test/metabase/api/dashboard_test.clj b/test/metabase/api/dashboard_test.clj
index 318ae703120254a547736ed089ee7dc229c0d856..a8239f377496cca7e6d1881510db7754e9826aef 100644
--- a/test/metabase/api/dashboard_test.clj
+++ b/test/metabase/api/dashboard_test.clj
@@ -4864,3 +4864,55 @@
             ;; dashcards and parameters for each dashcard, linked to a single card. Following is the proof
             ;; of things working as described.
             (is (= 1 @call-count))))))))
+
+;; Exception during scheduled (grouper) update of UserParameterValue is thrown. It is not relevant in context
+;; of tested functionality.
+;; TODO: Address the exception!
+(deftest dependent-dashcard-parameters-test
+  (mt/with-temp [:model/Card {card-id :id} {:name "c1"
+                                            :dataset_query (mt/mbql-query
+                                                             orders
+                                                             {:aggregation [[:count]]
+                                                              :breakout
+                                                              [!day.$created_at]})}
+                 :model/Dashboard {dashboard-id :id} {:name "d1"
+                                                      :parameters []}
+                 :model/DashboardCard {dashcard-id :id} {:card_id card-id
+                                                         :dashboard_id dashboard-id
+                                                         :parameter_mappings []}]
+    (t2/update! :model/Dashboard :id dashboard-id {:parameters [{:name "TIME Gr"
+                                                                 :slug "tgr"
+                                                                 :id "30d7efb0"
+                                                                 :type :temporal-unit
+                                                                 :sectionId "temporal-unit"}]})
+    (t2/update! :model/DashboardCard :id dashcard-id {:parameter_mappings [{:parameter_id "30d7efb0"
+                                                                            :type :temporal-unit
+                                                                            :card_id card-id
+                                                                            :target [:dimension
+                                                                                     (mt/$ids orders !day.$created_at)]}]})
+    (testing "Baseline"
+      (is (=? [["2016-04-01T00:00:00Z" 1]
+               ["2016-05-01T00:00:00Z" 19]]
+              (->> (mt/user-http-request
+                    :crowberto :post 202
+                    (format "dashboard/%d/dashcard/%d/card/%d/query" dashboard-id dashcard-id card-id)
+                    {:parameters [{:id "30d7efb0"
+                                   :type "temporal-unit"
+                                   :value "month"
+                                   :target
+                                   [:dimension
+                                    (mt/$ids orders !day.$created_at)]}]})
+                   mt/rows
+                   (take 2)))))
+    (mt/user-http-request
+     :crowberto :put 200
+     (format "card/%d" card-id)
+     {:dataset_query (mt/mbql-query
+                       orders
+                       {:aggregation [[:count]]
+                        :breakout
+                        [!year.$created_at]})})
+    (testing "Mapping is adjusted to new target (#49202)"
+      (is (= (mt/$ids orders !year.$created_at)
+             (t2/select-one-fn #(get-in % [:parameter_mappings 0 :target 1])
+                               :model/DashboardCard :id dashcard-id))))))
diff --git a/test/metabase/api/downloads_exports_test.clj b/test/metabase/api/downloads_exports_test.clj
index bd0f79f304ace8432fdeab550801648c41584f1a..5c1ff477535d0804d05c67166d1d16821a133eab 100644
--- a/test/metabase/api/downloads_exports_test.clj
+++ b/test/metabase/api/downloads_exports_test.clj
@@ -1061,32 +1061,108 @@
                                                        :type     :native
                                                        :native   {:query "SELECT 1234.567 as A"}}}]
         (testing "CSV downloads respect the formatted/unformatted setting"
-          (let [formatted-json-results   (all-downloads card {:export-format :csv :format-rows true})
-                unformatted-json-results (all-downloads card {:export-format :csv :format-rows false})]
+          (let [formatted-results   (all-downloads card {:export-format :csv :format-rows true})
+                unformatted-results (all-downloads card {:export-format :csv :format-rows false})]
             (is (= {:unsaved-card-download    [["A"] ["1,234.57"]]
                     :card-download            [["A"] ["1,234.57"]]
                     :public-question-download [["A"] ["1,234.57"]]
                     :dashcard-download        [["A"] ["1,234.57"]]
                     :public-dashcard-download [["A"] ["1,234.57"]]}
-                   formatted-json-results))
+                   formatted-results))
             (is (= {:unsaved-card-download    [["A"] ["1234.567"]]
                     :card-download            [["A"] ["1234.567"]]
                     :public-question-download [["A"] ["1234.567"]]
                     :dashcard-download        [["A"] ["1234.567"]]
                     :public-dashcard-download [["A"] ["1234.567"]]}
-                   unformatted-json-results))))
+                   unformatted-results))))
         (testing "JSON downloads respect the formatted/unformatted setting"
-          (let [formatted-json-results   (all-downloads card {:export-format :json :format-rows true})
-                unformatted-json-results (all-downloads card {:export-format :json :format-rows false})]
+          (let [formatted-results   (all-downloads card {:export-format :json :format-rows true})
+                unformatted-results (all-downloads card {:export-format :json :format-rows false})]
             (is (= {:unsaved-card-download    [["A"] ["1,234.57"]]
                     :card-download            [["A"] ["1,234.57"]]
                     :public-question-download [["A"] ["1,234.57"]]
                     :dashcard-download        [["A"] ["1,234.57"]]
                     :public-dashcard-download [["A"] ["1,234.57"]]}
-                   formatted-json-results))
+                   formatted-results))
             (is (= {:unsaved-card-download    [["A"] [1234.567]]
                     :card-download            [["A"] [1234.567]]
                     :public-question-download [["A"] [1234.567]]
                     :dashcard-download        [["A"] [1234.567]]
                     :public-dashcard-download [["A"] [1234.567]]}
-                   unformatted-json-results))))))))
+                   unformatted-results))))))))
+
+(deftest pivot-measures-order-test
+  (testing "A pivot download will use the user-configured measures order (#48442)."
+    (mt/dataset test-data
+      (mt/with-temp [:model/Card card {:display                :pivot
+                                       :dataset_query          {:database (mt/id)
+                                                                :type     :query
+                                                                :query
+                                                                {:source-table (mt/id :products)
+                                                                 :aggregation  [[:count]
+                                                                                [:sum [:field (mt/id :products :price) {:base-type :type/Float}]]
+                                                                                [:avg [:field (mt/id :products :rating) {:base-type :type/Float}]]]
+                                                                 :breakout     [[:field (mt/id :products :category) {:base-type :type/Text}]]}}
+                                       :visualization_settings {:pivot_table.column_split
+                                                                {:rows    [[:field (mt/id :products :category) {:base-type :type/Text}]]
+                                                                 :columns []
+                                                                 :values  [[:aggregation 1]
+                                                                           [:aggregation 0]
+                                                                           [:aggregation 2]]}
+                                                                :column_settings
+                                                                {"[\"name\",\"count\"]" {:column_title "Count Renamed"}
+                                                                 "[\"name\",\"sum\"]"   {:column_title "Sum Renamed"}
+                                                                 "[\"name\",\"avg\"]"   {:column_title "Average Renamed"}}}}]
+        (let [expected-header   ["Category" "Sum of Price" "Count" "Average of Rating"]
+              formatted-results (all-downloads card {:export-format :csv :format-rows false :pivot true})]
+          (is (= {:unsaved-card-download    expected-header
+                  :card-download            expected-header
+                  :public-question-download expected-header
+                  :dashcard-download        expected-header
+                  :public-dashcard-download expected-header}
+                 (update-vals formatted-results first))))
+        (testing "The column title changes are used when format-rows is true"
+          (let [expected-header   ["Category" "Sum Renamed" "Count Renamed" "Average Renamed"]
+                formatted-results (all-downloads card {:export-format :csv :format-rows true :pivot true})]
+            (is (= {:unsaved-card-download    expected-header
+                    :card-download            expected-header
+                    :public-question-download expected-header
+                    :dashcard-download        expected-header
+                    :public-dashcard-download expected-header}
+                   (update-vals formatted-results first)))))))))
+
+(deftest pivot-rows-order-test
+  (testing "A pivot download will use the user-configured rows order."
+    (mt/dataset test-data
+      (mt/with-temp [:model/Card card {:display                :pivot
+                                       :dataset_query          {:database (mt/id)
+                                                                :type     :query
+                                                                :query
+                                                                {:source-table (mt/id :products)
+                                                                 :aggregation  [[:count]]
+                                                                 :breakout     [[:field (mt/id :products :category) {:base-type :type/Text}]
+                                                                                [:field (mt/id :products :created_at) {:base-type :type/DateTime :temporal-unit :year}]]}}
+                                       :visualization_settings {:pivot_table.column_split
+                                                                {:rows    [[:field (mt/id :products :created_at) {:base-type :type/DateTime :temporal-unit :year}]
+                                                                           [:field (mt/id :products :category) {:base-type :type/Text}]]
+                                                                 :columns []
+                                                                 :values  [[:aggregation 0]]}
+                                                                :column_settings
+                                                                {"[\"name\",\"count\"]" {:column_title "Count Renamed"}}}}]
+        (let [expected-header   ["Created At" "Category" "Count"]
+              formatted-results (all-downloads card {:export-format :csv :format-rows false :pivot true})]
+          (is (= {:unsaved-card-download    expected-header
+                  :card-download            expected-header
+                  :public-question-download expected-header
+                  :dashcard-download        expected-header
+                  :public-dashcard-download expected-header}
+                 (update-vals formatted-results first))))
+        (testing "The column title changes are used when format-rows is true"
+          (let [expected-header   ["Created At" "Category" "Count Renamed"]
+                formatted-results (all-downloads card {:export-format :csv :format-rows true :pivot true})]
+            (is (= {:unsaved-card-download    expected-header
+                    :card-download            expected-header
+                    :public-question-download expected-header
+                    :dashcard-download        expected-header
+                    :public-dashcard-download expected-header}
+                   (update-vals formatted-results first)))))))))
diff --git a/test/metabase/api/pivots.clj b/test/metabase/api/pivots.clj
index 359228bafdae263cf4391e6f79c9d44504803e87..5b2541e49ada0bc1e44271a2cd408e26093037ff 100644
--- a/test/metabase/api/pivots.clj
+++ b/test/metabase/api/pivots.clj
@@ -5,9 +5,7 @@
 (defn applicable-drivers
   "Drivers that these pivot table tests should run on"
   []
-  (disj (mt/normal-drivers-with-feature :expressions :left-join)
-        ;; mongodb doesn't support foreign keys required by this test
-        :mongo
+  (disj (mt/normal-drivers-with-feature :expressions :left-join :metadata/key-constraints)
         ;; Disable on Redshift due to OutOfMemory issue (see #18834)
         :redshift))
 
diff --git a/test/metabase/events/view_log_test.clj b/test/metabase/events/view_log_test.clj
index 56448181b8d24d29a833350f27bc44ed49b1766b..a377b8e0e0ccb15ec112f158db1da770a0a351cd 100644
--- a/test/metabase/events/view_log_test.clj
+++ b/test/metabase/events/view_log_test.clj
@@ -61,7 +61,12 @@
              (latest-view (mt/user->id :crowberto) (u/id coll))))))))
 
 (deftest update-view-dashboard-timestamp-test
-  (let [now           (t/offset-date-time)
+  ;; the DB might save `last_used_at` with a different level of precision than the JVM does, on some machines
+  ;; `offset-date-time` returns nanosecond precision (9 decimal places) but `last_viewed_at` is coming back with
+  ;; microsecond precision (6 decimal places). We don't care about such a small difference, just strip it off of the
+  ;; times we're comparing.
+  (let [now           (-> (t/offset-date-time)
+                          (.withNano 0))
         one-hour-ago  (t/minus now (t/hours 1))
         two-hours-ago (t/minus now (t/hours 2))]
     (testing "update with multiple dashboards of the same ids will set timestamp to the latest"
diff --git a/test/metabase/models/card_test.clj b/test/metabase/models/card_test.clj
index dacc5199bb63779aaff74bea1bbd4230b8f47276..eee2dc0903cc050907e5ffceaeb0b337ab7375e4 100644
--- a/test/metabase/models/card_test.clj
+++ b/test/metabase/models/card_test.clj
@@ -1037,3 +1037,55 @@
           (mt/with-test-user :crowberto
             (is (false? (mi/can-read? card)))
             (is (false? (mi/can-write? card)))))))))
+
+(deftest breakouts-->identifier->action-fn-test
+  (are [b1 b2 expected--identifier->action] (= expected--identifier->action
+                                               (#'card/breakouts-->identifier->action b1 b2))
+    [[:field 10 {:temporal-unit :day}]]
+    nil
+    nil
+
+    [[:expression "x" {:temporal-unit :day}]]
+    nil
+    nil
+
+    [[:expression "x" {:temporal-unit :day}]]
+    [[:expression "x" {:temporal-unit :month}]]
+    {[:expression "x"] [:update [:expression "x" {:temporal-unit :month}]]}
+
+    [[:expression "x" {:temporal-unit :day}]]
+    [[:expression "x" {:temporal-unit :day}]]
+    nil
+
+    [[:field 10 {:temporal-unit :day}] [:expression "x" {:temporal-unit :day}]]
+    [[:expression "x" {:temporal-unit :day}] [:field 10 {:temporal-unit :month}]]
+    {[:field 10] [:update [:field 10 {:temporal-unit :month}]]}
+
+    [[:field 10 {:temporal-unit :year}] [:field 10 {:temporal-unit :day-of-week}]]
+    [[:field 10 {:temporal-unit :year}]]
+    nil))
+
+(deftest update-for-dashcard-fn-test
+  (are [indetifier->action quasi-dashcards expected-quasi-dashcards]
+       (= expected-quasi-dashcards
+          (#'card/updates-for-dashcards indetifier->action quasi-dashcards))
+
+    {[:field 10] [:update [:field 10 {:temporal-unit :month}]]}
+    [{:parameter_mappings []}]
+    nil
+
+    {[:field 10] [:update [:field 10 {:temporal-unit :month}]]}
+    [{:id 1 :parameter_mappings [{:target [:dimension [:field 10 nil]]}]}]
+    [[1 {:parameter_mappings [{:target [:dimension [:field 10 {:temporal-unit :month}]]}]}]]
+
+    {[:field 10] [:noop]}
+    [{:id 1 :parameter_mappings [{:target [:dimension [:field 10 nil]]}]}]
+    nil
+
+    {[:field 10] [:update [:field 10 {:temporal-unit :month}]]}
+    [{:id 1 :parameter_mappings [{:target [:dimension [:field 10 {:temporal-unit :year}]]}
+                                 {:target [:dimension [:field 33 {:temporal-unit :month}]]}
+                                 {:target [:dimension [:field 10 {:temporal-unit :day}]]}]}]
+    [[1 {:parameter_mappings [{:target [:dimension [:field 10 {:temporal-unit :month}]]}
+                              {:target [:dimension [:field 33 {:temporal-unit :month}]]}
+                              {:target [:dimension [:field 10 {:temporal-unit :month}]]}]}]]))
diff --git a/test/metabase/query_processor/pivot_test.clj b/test/metabase/query_processor/pivot_test.clj
index ca255bae82cd94174abd26a2a4a3796531325cb6..e2bf1cb002457cddd5fa1d93acbdb616b254ce3e 100644
--- a/test/metabase/query_processor/pivot_test.clj
+++ b/test/metabase/query_processor/pivot_test.clj
@@ -191,7 +191,7 @@
   (testing "`pivot-options` correctly generates pivot-rows and pivot-cols from a card's viz settings"
     (let [query         (api.pivots/pivot-query false)
           viz-settings  (:visualization_settings (api.pivots/pivot-card))
-          pivot-options {:pivot-rows [1 0], :pivot-cols [2]}]
+          pivot-options {:pivot-rows [1 0], :pivot-cols [2] :pivot-measures nil}]
       (is (= pivot-options
              (#'qp.pivot/pivot-options query viz-settings)))
       (are [num-breakouts expected] (= expected
@@ -217,7 +217,7 @@
                              :columns [[:field "RATING" {:base-type :type/Integer}]]}})
                          [:pivot_table.column_split])
           pivot-options (#'qp.pivot/pivot-options query viz-settings)]
-      (is (= {:pivot-rows [], :pivot-cols []}
+      (is (= {:pivot-rows [], :pivot-cols [] :pivot-measures nil}
              pivot-options))
       (is (= [[0 1] [1] [0] []]
              (#'qp.pivot/breakout-combinations 2 (:pivot-rows pivot-options) (:pivot-cols pivot-options)))))))
@@ -245,7 +245,7 @@
                                  {:rows    [$category]
                                   :columns [$created_at]}})
                 pivot-options (#'qp.pivot/pivot-options query viz-settings)]
-            (is (= {:pivot-rows [0], :pivot-cols [1]}
+            (is (= {:pivot-rows [0], :pivot-cols [1] :pivot-measures nil}
                    pivot-options))
             (is (= [[0 1] [1] [0] []]
                    (#'qp.pivot/breakout-combinations 2 (:pivot-rows pivot-options) (:pivot-cols pivot-options))))