From 460ac41f6f7a31c4f705c4bdfe53af8e8c65e164 Mon Sep 17 00:00:00 2001
From: metamben <103100869+metamben@users.noreply.github.com>
Date: Tue, 13 Aug 2024 21:21:37 +0300
Subject: [PATCH] Use the metric's source instead of the metric itself as base
 (#46688)

---
 .../metrics/metrics-editing.cy.spec.js        |  15 +-
 .../metrics/metrics-question.cy.spec.js       |   2 +-
 .../metrics/metrics-search.cy.spec.js         |  14 --
 frontend/src/metabase-lib/metrics.ts          |   4 -
 frontend/src/metabase-lib/v1/Question.ts      |   4 +
 .../metabase-lib/v1/metadata/utils/models.ts  |  24 +--
 .../v1/metadata/utils/models.unit.spec.js     |  29 ++--
 .../AggregationPicker/AggregationPicker.tsx   |  12 +-
 .../query_builder/actions/core/core.js        |   7 +-
 .../query_builder/actions/navigation.js       |  10 +-
 .../query_builder/actions/querying.js         |   9 +-
 .../components/notebook/lib/steps.ts          |   6 +-
 .../steps/DataStep/DataStep.unit.spec.tsx     |   1 -
 .../SummarizeContent/use-summarize-query.ts   |   2 +-
 .../src/metabase/query_builder/selectors.js   |   4 +-
 .../TableInteractive/TableInteractive.jsx     |   6 +-
 src/metabase/lib/query.cljc                   |  10 +-
 .../query_processor/middleware/metrics.clj    |  31 ++--
 test/metabase/lib/metric_test.cljc            |   2 +-
 test/metabase/lib/query_test.cljc             |  51 +++++++
 test/metabase/lib/remove_replace_test.cljc    |  23 +--
 .../middleware/metrics_test.clj               | 138 +++++++++---------
 22 files changed, 217 insertions(+), 187 deletions(-)

diff --git a/e2e/test/scenarios/metrics/metrics-editing.cy.spec.js b/e2e/test/scenarios/metrics/metrics-editing.cy.spec.js
index 487c9b7713a..166b79e467e 100644
--- a/e2e/test/scenarios/metrics/metrics-editing.cy.spec.js
+++ b/e2e/test/scenarios/metrics/metrics-editing.cy.spec.js
@@ -297,7 +297,7 @@ describe("scenarios > metrics > editing", () => {
       });
     });
 
-    it("should not be possible to join data on the first stage of a metric-based query", () => {
+    it("should be possible to join data on the first stage of a metric-based query", () => {
       createQuestion(ORDERS_SCALAR_METRIC);
       startNewQuestion();
       entityPickerModal().within(() => {
@@ -306,7 +306,7 @@ describe("scenarios > metrics > editing", () => {
       });
       getNotebookStep("data").within(() => {
         getActionButton("Custom column").should("be.visible");
-        getActionButton("Join data").should("not.exist");
+        getActionButton("Join data").should("be.visible");
       });
     });
   });
@@ -345,17 +345,6 @@ describe("scenarios > metrics > editing", () => {
       saveMetric();
       verifyScalarValue("111.38");
     });
-
-    it("should open the expression editor automatically when the source metric is already used in an aggregation expression", () => {
-      createQuestion(ORDERS_SCALAR_METRIC);
-      startNewMetric();
-      entityPickerModal().within(() => {
-        entityPickerModalTab("Metrics").click();
-        cy.findByText(ORDERS_SCALAR_METRIC.name).click();
-      });
-      startNewAggregation();
-      cy.findByTestId("expression-editor").should("be.visible");
-    });
   });
 
   describe("breakouts", () => {
diff --git a/e2e/test/scenarios/metrics/metrics-question.cy.spec.js b/e2e/test/scenarios/metrics/metrics-question.cy.spec.js
index b1c37b8c9cc..a86e8deea81 100644
--- a/e2e/test/scenarios/metrics/metrics-question.cy.spec.js
+++ b/e2e/test/scenarios/metrics/metrics-question.cy.spec.js
@@ -162,7 +162,7 @@ describe("scenarios > metrics > question", () => {
     cartesianChartCircle()
       .eq(23) // random dot
       .click({ force: true });
-    popover().findByText("See these records").click();
+    popover().findByText("See these Orders").click();
     cy.wait("@dataset");
     cy.findByTestId("qb-filters-panel")
       .findByText("Created At is Mar 1–31, 2024")
diff --git a/e2e/test/scenarios/metrics/metrics-search.cy.spec.js b/e2e/test/scenarios/metrics/metrics-search.cy.spec.js
index e1ade129124..3935b7a9337 100644
--- a/e2e/test/scenarios/metrics/metrics-search.cy.spec.js
+++ b/e2e/test/scenarios/metrics/metrics-search.cy.spec.js
@@ -76,18 +76,4 @@ describe("scenarios > metrics > search", () => {
     cy.wait("@dataset");
     cy.findByTestId("scalar-container").should("be.visible");
   });
-
-  it("should see metrics in recent items on the home page", () => {
-    createQuestion(ORDERS_SCALAR_METRIC).then(({ body: card }) => {
-      visitMetric(card.id);
-      cy.wait("@dataset");
-    });
-    navigationSidebar().findByText("Home").click();
-    cy.findByTestId("home-page").within(() => {
-      cy.findByText("Pick up where you left off").should("be.visible");
-      cy.findByText(ORDERS_SCALAR_METRIC.name).click();
-      cy.wait("@dataset");
-    });
-    cy.findByTestId("scalar-container").should("be.visible");
-  });
 });
diff --git a/frontend/src/metabase-lib/metrics.ts b/frontend/src/metabase-lib/metrics.ts
index 9ba892f5dfe..ca232fbe551 100644
--- a/frontend/src/metabase-lib/metrics.ts
+++ b/frontend/src/metabase-lib/metrics.ts
@@ -8,7 +8,3 @@ export function availableMetrics(
 ): MetricMetadata[] {
   return ML.available_metrics(query, stageIndex);
 }
-
-export function isMetricBased(query: Query, stageIndex: number): boolean {
-  return ML.metric_based_QMARK_(query, stageIndex);
-}
diff --git a/frontend/src/metabase-lib/v1/Question.ts b/frontend/src/metabase-lib/v1/Question.ts
index 76dddd5a338..874fbe12ad7 100644
--- a/frontend/src/metabase-lib/v1/Question.ts
+++ b/frontend/src/metabase-lib/v1/Question.ts
@@ -459,6 +459,10 @@ class Question {
     const metadata = this.metadataProvider();
     const tableId = getQuestionVirtualTableId(this.id());
     const table = Lib.tableOrCardMetadata(metadata, tableId);
+    if (!table) {
+      return this;
+    }
+
     const query = Lib.queryFromTableOrCardMetadata(metadata, table);
     return this.setQuery(query);
   }
diff --git a/frontend/src/metabase-lib/v1/metadata/utils/models.ts b/frontend/src/metabase-lib/v1/metadata/utils/models.ts
index 7d72b9ac012..2ff77730a32 100644
--- a/frontend/src/metabase-lib/v1/metadata/utils/models.ts
+++ b/frontend/src/metabase-lib/v1/metadata/utils/models.ts
@@ -1,7 +1,6 @@
 import * as Lib from "metabase-lib";
 import type Question from "metabase-lib/v1/Question";
 import type Database from "metabase-lib/v1/metadata/Database";
-import { getQuestionVirtualTableId } from "metabase-lib/v1/metadata/utils/saved-questions";
 import type NativeQuery from "metabase-lib/v1/queries/NativeQuery";
 import { findColumnIndexesForColumnSettings } from "metabase-lib/v1/queries/utils/dataset";
 import type {
@@ -101,22 +100,23 @@ export function checkCanBeModel(question: Question) {
     .every(isSupportedTemplateTagForModel);
 }
 
-export function isAdHocModelQuestion(
+export function isAdHocModelOrMetricQuestion(
   question?: Question,
   originalQuestion?: Question,
 ) {
-  if (!question || !originalQuestion) {
-    return false;
+  if (
+    question &&
+    originalQuestion &&
+    question.id() === originalQuestion.id() &&
+    (question.type() !== "question" || originalQuestion.type() !== "question")
+  ) {
+    return Lib.areLegacyQueriesEqual(
+      question.datasetQuery(),
+      originalQuestion.composeQuestion().datasetQuery(),
+    );
   }
 
-  const isModel =
-    question.type() !== "question" || originalQuestion.type() !== "question";
-  const isSameQuestion = question.id() === originalQuestion.id();
-  const isSelfReferencing =
-    Lib.sourceTableOrCardId(question.query()) ===
-    getQuestionVirtualTableId(originalQuestion.id());
-
-  return isModel && isSameQuestion && isSelfReferencing;
+  return false;
 }
 
 export function checkCanRefreshModelCache(
diff --git a/frontend/src/metabase-lib/v1/metadata/utils/models.unit.spec.js b/frontend/src/metabase-lib/v1/metadata/utils/models.unit.spec.js
index fd0ceafcaa1..b7c60d23df5 100644
--- a/frontend/src/metabase-lib/v1/metadata/utils/models.unit.spec.js
+++ b/frontend/src/metabase-lib/v1/metadata/utils/models.unit.spec.js
@@ -6,7 +6,7 @@ import {
   checkCanBeModel,
   checkCanRefreshModelCache,
   getModelCacheSchemaName,
-  isAdHocModelQuestion,
+  isAdHocModelOrMetricQuestion,
   getDatasetMetadataCompletenessPercentage,
 } from "metabase-lib/v1/metadata/utils/models";
 import {
@@ -200,7 +200,7 @@ describe("data model utils", () => {
       const { metadata } = setup({ cards: [modelCard] });
       const question = new Question(composedModelCard, metadata);
 
-      expect(isAdHocModelQuestion(question)).toBe(false);
+      expect(isAdHocModelOrMetricQuestion(question)).toBe(false);
     });
 
     it("returns false for native questions", () => {
@@ -209,21 +209,18 @@ describe("data model utils", () => {
 
       const question = metadata.question(card.id);
 
-      expect(isAdHocModelQuestion(question, question)).toBe(false);
+      expect(isAdHocModelOrMetricQuestion(question, question)).toBe(false);
     });
 
     it("identifies when model goes into ad-hoc exploration mode", () => {
       const modelCard = createStructuredModelCard({ id: 1 });
-      const composedModelCard = createSavedStructuredCard({
-        id: 1,
-        sourceTable: "card__1",
-      });
       const { metadata } = setup({ cards: [modelCard] });
-
       const originalQuestion = metadata.question(modelCard.id);
-      const question = new Question(composedModelCard, metadata);
+      const question = originalQuestion.composeQuestion();
 
-      expect(isAdHocModelQuestion(question, originalQuestion)).toBe(true);
+      expect(isAdHocModelOrMetricQuestion(question, originalQuestion)).toBe(
+        true,
+      );
     });
 
     it("returns false when IDs don't match", () => {
@@ -237,7 +234,9 @@ describe("data model utils", () => {
       const originalQuestion = metadata.question(modelCard.id);
       const question = new Question(composedModelCard, metadata);
 
-      expect(isAdHocModelQuestion(question, originalQuestion)).toBe(false);
+      expect(isAdHocModelOrMetricQuestion(question, originalQuestion)).toBe(
+        false,
+      );
     });
 
     it("returns false when questions are not models", () => {
@@ -251,7 +250,9 @@ describe("data model utils", () => {
       const originalQuestion = metadata.question(modelCard.id);
       const question = new Question(composedModelCard, metadata);
 
-      expect(isAdHocModelQuestion(question, originalQuestion)).toBe(false);
+      expect(isAdHocModelOrMetricQuestion(question, originalQuestion)).toBe(
+        false,
+      );
     });
 
     it("returns false when potential ad-hoc model question is not self-referencing", () => {
@@ -265,7 +266,9 @@ describe("data model utils", () => {
       const originalQuestion = metadata.question(modelCard.id);
       const question = new Question(composedModelCard, metadata);
 
-      expect(isAdHocModelQuestion(question, originalQuestion)).toBe(false);
+      expect(isAdHocModelOrMetricQuestion(question, originalQuestion)).toBe(
+        false,
+      );
     });
   });
 
diff --git a/frontend/src/metabase/common/components/AggregationPicker/AggregationPicker.tsx b/frontend/src/metabase/common/components/AggregationPicker/AggregationPicker.tsx
index 3392269f32f..4f9f7452e7e 100644
--- a/frontend/src/metabase/common/components/AggregationPicker/AggregationPicker.tsx
+++ b/frontend/src/metabase/common/components/AggregationPicker/AggregationPicker.tsx
@@ -108,9 +108,8 @@ export function AggregationPicker({
     const databaseId = Lib.databaseID(query);
     const database = metadata.database(databaseId);
     const canUseExpressions = database?.hasFeature("expression-aggregations");
-    const isMetricBased = Lib.isMetricBased(query, stageIndex);
 
-    if (operators.length > 0 && !isMetricBased) {
+    if (operators.length > 0) {
       const operatorItems = operators.map(operator =>
         getOperatorListItem(query, stageIndex, operator),
       );
@@ -126,7 +125,7 @@ export function AggregationPicker({
     if (metrics.length > 0) {
       sections.push({
         key: "metrics",
-        name: isMetricBased ? t`Metrics` : t`Common Metrics`,
+        name: t`Common Metrics`,
         items: metrics.map(metric =>
           getMetricListItem(query, stageIndex, metric, clauseIndex),
         ),
@@ -371,12 +370,7 @@ function isExpressionEditorInitiallyOpen(
   operators: Lib.AggregationOperator[],
 ): boolean {
   if (!clause) {
-    return (
-      Lib.isMetricBased(query, stageIndex) &&
-      Lib.availableMetrics(query, stageIndex)
-        .map(metric => Lib.displayInfo(query, stageIndex, metric))
-        .every(metricInfo => metricInfo.aggregationPosition != null)
-    );
+    return false;
   }
 
   const initialOperator = getInitialOperator(query, stageIndex, operators);
diff --git a/frontend/src/metabase/query_builder/actions/core/core.js b/frontend/src/metabase/query_builder/actions/core/core.js
index e0d7e9920cf..8294ca46eca 100644
--- a/frontend/src/metabase/query_builder/actions/core/core.js
+++ b/frontend/src/metabase/query_builder/actions/core/core.js
@@ -18,7 +18,7 @@ import { getMetadata } from "metabase/selectors/metadata";
 import { getCardAfterVisualizationClick } from "metabase/visualizations/lib/utils";
 import * as Lib from "metabase-lib";
 import Question from "metabase-lib/v1/Question";
-import { isAdHocModelQuestion } from "metabase-lib/v1/metadata/utils/models";
+import { isAdHocModelOrMetricQuestion } from "metabase-lib/v1/metadata/utils/models";
 import Query from "metabase-lib/v1/queries/Query";
 import {
   cardIsEquivalent,
@@ -257,7 +257,10 @@ export const apiUpdateQuestion = (question, { rerunQuery } = {}) => {
       submittableQuestion,
       dispatch,
       {
-        excludeDatasetQuery: isAdHocModelQuestion(question, originalQuestion),
+        excludeDatasetQuery: isAdHocModelOrMetricQuestion(
+          question,
+          originalQuestion,
+        ),
       },
     );
 
diff --git a/frontend/src/metabase/query_builder/actions/navigation.js b/frontend/src/metabase/query_builder/actions/navigation.js
index 340d2b09728..6b42db3186d 100644
--- a/frontend/src/metabase/query_builder/actions/navigation.js
+++ b/frontend/src/metabase/query_builder/actions/navigation.js
@@ -7,7 +7,7 @@ import { createThunkAction } from "metabase/lib/redux";
 import { equals } from "metabase/lib/utils";
 import { getLocation } from "metabase/selectors/routing";
 import * as Lib from "metabase-lib";
-import { isAdHocModelQuestion } from "metabase-lib/v1/metadata/utils/models";
+import { isAdHocModelOrMetricQuestion } from "metabase-lib/v1/metadata/utils/models";
 
 import {
   getCard,
@@ -142,10 +142,14 @@ export const updateUrl = createThunkAction(
 
       if (dirty == null) {
         const originalQuestion = getOriginalQuestion(getState());
-        const isAdHocModel = isAdHocModelQuestion(question, originalQuestion);
+        const isAdHocModelOrMetric = isAdHocModelOrMetricQuestion(
+          question,
+          originalQuestion,
+        );
         dirty =
           !originalQuestion ||
-          (!isAdHocModel && question.isDirtyComparedTo(originalQuestion));
+          (!isAdHocModelOrMetric &&
+            question.isDirtyComparedTo(originalQuestion));
       }
 
       const { isNative } = Lib.queryDisplayInfo(question.query());
diff --git a/frontend/src/metabase/query_builder/actions/querying.js b/frontend/src/metabase/query_builder/actions/querying.js
index 900467352d7..07b84396774 100644
--- a/frontend/src/metabase/query_builder/actions/querying.js
+++ b/frontend/src/metabase/query_builder/actions/querying.js
@@ -10,7 +10,7 @@ import { getWhiteLabeledLoadingMessageFactory } from "metabase/selectors/whitela
 import { runQuestionQuery as apiRunQuestionQuery } from "metabase/services";
 import { getSensibleDisplays } from "metabase/visualizations";
 import * as Lib from "metabase-lib";
-import { isAdHocModelQuestion } from "metabase-lib/v1/metadata/utils/models";
+import { isAdHocModelOrMetricQuestion } from "metabase-lib/v1/metadata/utils/models";
 
 import {
   getCard,
@@ -114,9 +114,10 @@ export const runQuestionQuery = ({
       : true;
 
     if (shouldUpdateUrl) {
-      const isAdHocModelOrMetric =
-        (question.type() === "model" || question.type() === "metric") &&
-        isAdHocModelQuestion(question, originalQuestion);
+      const isAdHocModelOrMetric = isAdHocModelOrMetricQuestion(
+        question,
+        originalQuestion,
+      );
 
       dispatch(
         updateUrl(question, { dirty: !isAdHocModelOrMetric && isCardDirty }),
diff --git a/frontend/src/metabase/query_builder/components/notebook/lib/steps.ts b/frontend/src/metabase/query_builder/components/notebook/lib/steps.ts
index ccbc2dba3d2..967984bf92b 100644
--- a/frontend/src/metabase/query_builder/components/notebook/lib/steps.ts
+++ b/frontend/src/metabase/query_builder/components/notebook/lib/steps.ts
@@ -33,11 +33,7 @@ const STEPS: NotebookStepDef[] = [
     clauseType: "joins",
     valid: (query, stageIndex, metadata) => {
       const database = metadata.database(Lib.databaseID(query));
-      return (
-        hasData(query) &&
-        Boolean(database?.hasFeature("join")) &&
-        !Lib.isMetricBased(query, stageIndex)
-      );
+      return hasData(query) && Boolean(database?.hasFeature("join"));
     },
     subSteps: (query, stageIndex) => {
       return Lib.joins(query, stageIndex).length;
diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/DataStep/DataStep.unit.spec.tsx b/frontend/src/metabase/query_builder/components/notebook/steps/DataStep/DataStep.unit.spec.tsx
index c602714fbf7..29a58d5be38 100644
--- a/frontend/src/metabase/query_builder/components/notebook/steps/DataStep/DataStep.unit.spec.tsx
+++ b/frontend/src/metabase/query_builder/components/notebook/steps/DataStep/DataStep.unit.spec.tsx
@@ -164,7 +164,6 @@ describe("DataStep", () => {
   it.each<{ type: CardType; icon: IconName }>([
     { type: "question", icon: "table2" },
     { type: "model", icon: "model" },
-    { type: "metric", icon: "metric" },
   ])("should render with a selected card", ({ type, icon }) => {
     const card = createSavedStructuredCard({
       id: 1,
diff --git a/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/SummarizeContent/use-summarize-query.ts b/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/SummarizeContent/use-summarize-query.ts
index b68c5d75ca7..6b04d5d9c6c 100644
--- a/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/SummarizeContent/use-summarize-query.ts
+++ b/frontend/src/metabase/query_builder/components/view/sidebars/SummarizeSidebar/SummarizeContent/use-summarize-query.ts
@@ -119,5 +119,5 @@ export const useSummarizeQuery = ({
 
 function shouldAddDefaultAggregation(query: Lib.Query): boolean {
   const aggregations = Lib.aggregations(query, STAGE_INDEX);
-  return aggregations.length === 0 && !Lib.isMetricBased(query, STAGE_INDEX);
+  return aggregations.length === 0;
 }
diff --git a/frontend/src/metabase/query_builder/selectors.js b/frontend/src/metabase/query_builder/selectors.js
index 90d61f2cb05..8a6f5e06233 100644
--- a/frontend/src/metabase/query_builder/selectors.js
+++ b/frontend/src/metabase/query_builder/selectors.js
@@ -37,7 +37,7 @@ import {
   isTimeseries,
 } from "metabase/visualizations/lib/renderer_utils";
 import { isAbsoluteDateTimeUnit } from "metabase-types/guards/date-time";
-import { isAdHocModelQuestion } from "metabase-lib/v1/metadata/utils/models";
+import { isAdHocModelOrMetricQuestion } from "metabase-lib/v1/metadata/utils/models";
 import { getCardUiParameters } from "metabase-lib/v1/parameters/utils/cards";
 import {
   normalizeParameters,
@@ -602,7 +602,7 @@ export const getIsDirty = createSelector(
     // We need to escape the isDirty check as it will always be true in this case,
     // and the page will always be covered with a 'rerun' overlay.
     // Once the dataset_query changes, the question will loose the "dataset" flag and it'll work normally
-    if (!question || isAdHocModelQuestion(question, originalQuestion)) {
+    if (!question || isAdHocModelOrMetricQuestion(question, originalQuestion)) {
       return false;
     }
     return question.isDirtyComparedToWithoutParameters(originalQuestion);
diff --git a/frontend/src/metabase/visualizations/components/TableInteractive/TableInteractive.jsx b/frontend/src/metabase/visualizations/components/TableInteractive/TableInteractive.jsx
index cefbd602d53..34364718fbf 100644
--- a/frontend/src/metabase/visualizations/components/TableInteractive/TableInteractive.jsx
+++ b/frontend/src/metabase/visualizations/components/TableInteractive/TableInteractive.jsx
@@ -44,7 +44,7 @@ import {
 } from "metabase/visualizations/lib/table";
 import { getColumnExtent } from "metabase/visualizations/lib/utils";
 import * as Lib from "metabase-lib";
-import { isAdHocModelQuestion } from "metabase-lib/v1/metadata/utils/models";
+import { isAdHocModelOrMetricQuestion } from "metabase-lib/v1/metadata/utils/models";
 import { isID, isPK, isFK } from "metabase-lib/v1/types/utils/isa";
 import { memoizeClass } from "metabase-lib/v1/utils";
 
@@ -214,8 +214,8 @@ class TableInteractive extends Component {
     const isDataChange =
       data && nextData && !_.isEqual(data.cols, nextData.cols);
     const isDatasetStatusChange =
-      isAdHocModelQuestion(nextQuestion, question) ||
-      isAdHocModelQuestion(question, nextQuestion);
+      isAdHocModelOrMetricQuestion(nextQuestion, question) ||
+      isAdHocModelOrMetricQuestion(question, nextQuestion);
 
     if (isDataChange && !isDatasetStatusChange) {
       this.resetColumnWidths();
diff --git a/src/metabase/lib/query.cljc b/src/metabase/lib/query.cljc
index 364e9e19c82..295a9e029b0 100644
--- a/src/metabase/lib/query.cljc
+++ b/src/metabase/lib/query.cljc
@@ -235,16 +235,14 @@
 (defn- metric-query
   [metadata-providerable card-metadata]
   (let [card-id (u/the-id card-metadata)
+        metric-first-stage (-> (query metadata-providerable (:dataset-query card-metadata))
+                               (lib.util/query-stage 0))
         base-query (query-with-stages metadata-providerable
-                                      [{:lib/type :mbql.stage/mbql
-                                        :source-card card-id}])
-        metric-breakouts (-> (query metadata-providerable (:dataset-query card-metadata))
-                             (lib.util/query-stage -1)
-                             :breakout)
+                                      [(select-keys metric-first-stage [:lib/type :source-card :source-table])])
         base-query (reduce
                     #(lib.util/add-summary-clause %1 0 :breakout %2)
                     base-query
-                    metric-breakouts)]
+                    (:breakout metric-first-stage))]
     (-> base-query
         (lib.util/add-summary-clause
          0 :aggregation
diff --git a/src/metabase/query_processor/middleware/metrics.clj b/src/metabase/query_processor/middleware/metrics.clj
index 831e56a9fa8..d84a9cde8ee 100644
--- a/src/metabase/query_processor/middleware/metrics.clj
+++ b/src/metabase/query_processor/middleware/metrics.clj
@@ -93,24 +93,33 @@
 
 (defn- aggregation-stage-index
   [stages]
-  (count (take-while :qp/stage-is-from-source-card stages)))
+  (count (take-while (complement (comp find-metric-ids :aggregation)) stages)))
+
+(defn- add-join-aliases
+  [x source-field->join-alias]
+  (lib.util.match/replace x
+    [:field (opts :guard (every-pred (comp source-field->join-alias :source-field) (complement :join-alias))) _]
+    (assoc-in &match [1 :join-alias] (-> opts :source-field source-field->join-alias))))
 
 (defn- include-implicit-joins
   [query agg-stage-index metric-query]
   (let [metric-joins (lib/joins metric-query -1)
         existing-joins (into #{}
                              (map (juxt :fk-field-id :alias))
-                             (lib/joins query agg-stage-index))]
-    (reduce #(lib/join %1 agg-stage-index %2)
-            query
-            (remove (comp existing-joins (juxt :fk-field-id :alias)) metric-joins))))
+                             (lib/joins query agg-stage-index))
+        new-joins (remove (comp existing-joins (juxt :fk-field-id :alias)) metric-joins)
+        source-field->join-alias (into {} (map (juxt :fk-field-id :alias)) new-joins)
+        query-with-joins (reduce #(lib/join %1 agg-stage-index %2)
+                                 query
+                                 new-joins)]
+    (lib.util/update-query-stage query-with-joins agg-stage-index add-join-aliases source-field->join-alias)))
 
 (defn splice-compatible-metrics
   "Splices in metric definitions that are compatible with the query."
   [query path expanded-stages]
-  (let [agg-stage-index (aggregation-stage-index (:stages query))]
+  (let [agg-stage-index (aggregation-stage-index expanded-stages)]
     (if-let [lookup (->> expanded-stages
-                         (drop-while :qp/stage-is-from-source-card)
+                         (drop agg-stage-index)
                          first
                          :aggregation
                          (fetch-referenced-metrics query))]
@@ -237,10 +246,10 @@
       A query built from a `:source-card` of `:type :metric` can reference itself."
   [query]
   (let [query (lib.walk/walk
-                query
-                (fn [_query path-type path stage-or-join]
-                  (when (= path-type :lib.walk/join)
-                    (update stage-or-join :stages #(adjust-metric-stages query path %)))))]
+               query
+               (fn [_query path-type path stage-or-join]
+                 (when (= path-type :lib.walk/join)
+                   (update stage-or-join :stages #(adjust-metric-stages query path %)))))]
     (u/prog1
       (update query :stages #(adjust-metric-stages query nil %))
       (when-let [metric (lib.util.match/match-one <>
diff --git a/test/metabase/lib/metric_test.cljc b/test/metabase/lib/metric_test.cljc
index 5081d8b4ac7..4d141a164cd 100644
--- a/test/metabase/lib/metric_test.cljc
+++ b/test/metabase/lib/metric_test.cljc
@@ -171,7 +171,7 @@
                :database (meta/id)
                :stages
                [{:lib/type :mbql.stage/mbql
-                 :source-card 1
+                 :source-table (meta/id :orders)
                  :breakout
                  [[:field
                    {:base-type :type/DateTimeWithLocalTZ, :temporal-unit :month}
diff --git a/test/metabase/lib/query_test.cljc b/test/metabase/lib/query_test.cljc
index 5d6e17292f5..82b5322f7b3 100644
--- a/test/metabase/lib/query_test.cljc
+++ b/test/metabase/lib/query_test.cljc
@@ -568,3 +568,54 @@
                  (is (= exp-fields
                         (into #{} (map :id) (lib/visible-columns query2))))
                  (is (= 1 @calls))))))))))
+
+(deftest ^:parallel metric-based-question-test
+  (let [question-id 100
+        model-id 101
+        table-based-metric-id 102
+        model-based-metric-id 103
+        base-card {:name        "Sum of Cans"
+                   :database-id (meta/id)
+                   :table-id    (meta/id :venues)
+                   :dataset-query
+                   (-> lib.tu/venues-query
+                       (lib/filter (lib/= (meta/field-metadata :venues :price) 4))
+                       (lib/aggregate (lib/sum (meta/field-metadata :venues :price)))
+                       (lib/breakout (meta/field-metadata :venues :category-id))
+                       (lib/breakout (meta/field-metadata :venues :latitude))
+                       (lib/breakout (meta/field-metadata :venues :longitude))
+                       lib.convert/->legacy-MBQL)}
+        base-mp (lib.tu/mock-metadata-provider
+                 meta/metadata-provider
+                 {:cards [(assoc base-card :id question-id           :type :question)
+                          (assoc base-card :id model-id              :type :model)
+                          (assoc base-card :id table-based-metric-id :type :metric)]})
+        mp (lib.tu/mock-metadata-provider
+            base-mp
+            {:cards [{:id          model-based-metric-id
+                      :name        "Model based metric"
+                      :database-id (meta/id)
+                      :table-id    (meta/id :venues)
+                      :source-card-id model-id
+                      :dataset-query
+                      (-> (lib/query base-mp (lib.metadata/card base-mp model-id))
+                          (lib/aggregate (lib/count))
+                          lib.convert/->legacy-MBQL)
+                      :type :metric}]})]
+    (is (=? {:lib/type :mbql/query
+             :database (meta/id)
+             :stages
+             [{:lib/type :mbql.stage/mbql
+               :source-table (meta/id :venues)
+               :aggregation [[:metric {} table-based-metric-id]]
+               :breakout [[:field {} (meta/id :venues :category-id)]
+                          [:field {} (meta/id :venues :latitude)]
+                          [:field {} (meta/id :venues :longitude)]]}]}
+            (lib/query base-mp (lib.metadata/card base-mp table-based-metric-id))))
+    (is (=? {:lib/type :mbql/query
+             :database (meta/id)
+             :stages
+             [{:lib/type :mbql.stage/mbql
+               :source-card model-id
+               :aggregation [[:metric {} model-based-metric-id]]}]}
+            (lib/query base-mp (lib.metadata/card mp model-based-metric-id))))))
diff --git a/test/metabase/lib/remove_replace_test.cljc b/test/metabase/lib/remove_replace_test.cljc
index 9dd7ea2668a..eb279fce8f6 100644
--- a/test/metabase/lib/remove_replace_test.cljc
+++ b/test/metabase/lib/remove_replace_test.cljc
@@ -483,6 +483,7 @@
                              {:cards [{:id          100
                                        :name        "Sum of Cans"
                                        :database-id (meta/id)
+                                       :table-id    (meta/id :venues)
                                        :dataset-query
                                        (-> lib.tu/venues-query
                                            (lib/filter (lib/= (meta/field-metadata :venues :price) 4))
@@ -496,18 +497,18 @@
                                        [:count {:lib/uuid string?}]]}]}
               query))
       (is (=? {:stages [{:aggregation [[:metric {:lib/uuid string?} 100]
-                                       [:metric {:lib/uuid string?} 100]]}]}
-              (lib/replace-clause
-               query
-               (second (lib/aggregations query))
-               (first (lib/available-metrics query)))))
+                                         [:metric {:lib/uuid string?} 100]]}]}
+                (lib/replace-clause
+                 query
+                 (second (lib/aggregations query))
+                 (first (lib/available-metrics query)))))
       (is (=? {:stages [{:aggregation [[:count {:lib/uuid string?}]
-                                       [:metric {:lib/uuid string?} 100]]}]}
-              (-> query
-                  (lib/replace-clause
-                   (second (lib/aggregations query))
-                   (first (lib/available-metrics query)))
-                  (as-> $q (lib/replace-clause $q (first (lib/aggregations $q)) (lib/count)))))))))
+                                         [:metric {:lib/uuid string?} 100]]}]}
+                (-> query
+                    (lib/replace-clause
+                     (second (lib/aggregations query))
+                     (first (lib/available-metrics query)))
+                    (as-> $q (lib/replace-clause $q (first (lib/aggregations $q)) (lib/count)))))))))
 
 (deftest ^:parallel replace-segment-test
   (testing "replacing with segment should work"
diff --git a/test/metabase/query_processor/middleware/metrics_test.clj b/test/metabase/query_processor/middleware/metrics_test.clj
index 5e3d3b7e275..e1b256606a6 100644
--- a/test/metabase/query_processor/middleware/metrics_test.clj
+++ b/test/metabase/query_processor/middleware/metrics_test.clj
@@ -75,10 +75,9 @@
 (deftest ^:parallel adjust-basic-source-metric-test
   (let [[source-metric mp] (mock-metric)
         query (lib/query mp source-metric)]
-    (is (=?
-          {:stages [{:source-table (meta/id :products)}
-                    {:aggregation [[:avg {} [:field {} (comp #{"rating"} u/lower-case-en)]]]}]}
-          (adjust query)))))
+    (is (=? {:stages [{:source-table (meta/id :products)
+                       :aggregation [[:avg {} [:field {} (meta/id :products :rating)]]]}]}
+            (adjust query)))))
 
 (deftest ^:parallel adjust-aggregation-metric-ref-test
   (let [[source-metric mp] (mock-metric)
@@ -169,30 +168,29 @@
         query (-> (lib/query mp source-metric)
                   (lib/join (-> (lib/join-clause (meta/table-metadata :orders)
                                                  [(lib/=
-                                                    (meta/field-metadata :products :id)
-                                                    (meta/field-metadata :orders :product-id))])
+                                                   (meta/field-metadata :products :id)
+                                                   (meta/field-metadata :orders :product-id))])
                                 (lib/with-join-fields :all))))]
-    (is (=?
-          {:stages [{:source-table (meta/id :products)}
-                    {:aggregation [[:avg {} [:field {} (comp #{"rating"} u/lower-case-en)]]]
-                     :joins [{:stages
-                              [{:source-table (meta/id :orders)}],
-                              :conditions
-                              [[:= {}
-                                [:field {} (meta/id :products :id)]
-                                [:field {:join-alias "Orders"} (meta/id :orders :product-id)]]],
-                              :alias "Orders"}]}]}
-          (adjust query)))))
+    (is (=? {:stages [{:source-table (meta/id :products)
+                       :aggregation [[:avg {} [:field {} (meta/id :products :rating)]]]
+                       :joins [{:stages
+                                [{:source-table (meta/id :orders)}],
+                                :conditions
+                                [[:= {}
+                                  [:field {} (meta/id :products :id)]
+                                  [:field {:join-alias "Orders"} (meta/id :orders :product-id)]]],
+                                :alias "Orders"}]}]}
+            (adjust query)))))
 
 (deftest ^:parallel adjust-expression-test
   (let [[source-metric mp] (mock-metric (lib/expression (basic-metric-query) "source" (lib/+ 1 1)))
         query (-> (lib/query mp source-metric)
                   (lib/expression "target" (lib/- 2 2)))]
     (is (=?
-          {:stages [{:expressions [[:+ {:lib/expression-name "source"} 1 1]]}
-                    {:expressions [[:- {:lib/expression-name "target"} 2 2]]
-                     :aggregation [[:avg {} [:field {} (comp #{"rating"} u/lower-case-en)]]]}]}
-          (adjust query)))))
+         {:stages [{:expressions [[:- {:lib/expression-name "target"} 2 2]
+                                  [:+ {:lib/expression-name "source"} 1 1]]
+                    :aggregation [[:avg {} [:field {} (meta/id :products :rating)]]]}]}
+         (adjust query)))))
 
 (deftest ^:parallel adjust-expression-name-collision-test
   (let [[source-metric mp] (mock-metric (-> (basic-metric-query)
@@ -221,10 +219,10 @@
         query (-> (lib/query mp source-metric)
                   (lib/filter (lib/= (meta/field-metadata :products :category) "Widget")))]
     (is (=?
-          {:stages [{:source-table (meta/id :products)
-                     :filters [[:> {} [:field {} (meta/id :products :price)] 1]]}
-                    {:filters [[:= {} [:field {} (meta/id :products :category)] "Widget"]]}]}
-          (adjust query)))))
+         {:stages [{:source-table (meta/id :products)
+                    :filters [[:= {} [:field {} (meta/id :products :category)] "Widget"]
+                              [:> {} [:field {} (meta/id :products :price)] [:value {} 1]]]}]}
+         (adjust query)))))
 
 (deftest ^:parallel adjust-mixed-multi-source-test
   (let [[first-metric mp] (mock-metric lib.tu/metadata-provider-with-mock-cards
@@ -236,11 +234,9 @@
         query (-> (lib/query mp second-metric)
                   (lib/filter (lib/= (meta/field-metadata :products :category) "Widget")))]
     (is (=? {:stages [{:source-table (meta/id :products)}
-                      {:filters [[:> {} [:field {} (meta/id :products :price)] 1]]
-                       :aggregation complement}
-                      {:filters [[:< {} [:field {} (meta/id :products :price)] 100]]
-                       :aggregation complement}
-                      {:filters [[:= {} [:field {} (meta/id :products :category)] "Widget"]]
+                      {:filters [[:= {} [:field {} (meta/id :products :category)] "Widget"]
+                                 [:< {} [:field {} (meta/id :products :price)] [:value {} 100]]
+                                 [:> {} [:field {} (meta/id :products :price)] [:value {} 1]]]
                        :aggregation some?}]}
             (adjust query)))))
 
@@ -249,10 +245,7 @@
         [second-metric mp] (mock-metric mp (lib/query mp first-metric))
         [third-metric mp] (mock-metric mp (lib/query mp second-metric))
         query (lib/query mp third-metric)]
-    (is (=? {:stages [{:aggregation complement}
-                      {:aggregation complement}
-                      {:aggregation complement}
-                      {:aggregation [[:avg {} [:field {} (comp #{"rating"} u/lower-case-en)]]]}]}
+    (is (=? {:stages [{:aggregation [[:avg {} [:field {} (meta/id :products :rating)]]]}]}
             (adjust query)))))
 
 (deftest ^:parallel joined-question-based-on-metric-based-on-metric-based-on-metric-test
@@ -261,9 +254,7 @@
         [question mp] (mock-metric mp (lib/query mp second-metric) {:type :question})
         query (-> (lib/query mp (meta/table-metadata :products))
                   (lib/join (lib/join-clause question [(lib/= 1 1)])))]
-    (is (=? {:stages [{:joins [{:stages [{:aggregation complement}
-                                         {:aggregation complement}
-                                         {:aggregation [[:avg {} [:field {} (comp #{"rating"} u/lower-case-en)]]]}
+    (is (=? {:stages [{:joins [{:stages [{:aggregation [[:avg {} [:field {} (meta/id :products :rating)]]]}
                                          ;; Empty stage added by resolved-source-cards to nest join
                                          #(= #{:lib/type :qp/stage-had-source-card :source-query/model?} (set (keys %)))]}]}]}
             (adjust query)))))
@@ -385,8 +376,7 @@
     (let [[source-metric mp] (mock-metric)
           query (-> (lib/query mp source-metric)
                     (as-> $q (lib/order-by $q (lib/aggregation-ref $q 0))))]
-      (is (=? {:stages [{}
-                        {:aggregation [[:avg {:lib/uuid (=?/same :uuid)} some?]]
+      (is (=? {:stages [{:aggregation [[:avg {:lib/uuid (=?/same :uuid)} some?]]
                          :order-by [[:asc {} [:aggregation {} (=?/same :uuid)]]]}]}
               (adjust query))))))
 
@@ -439,26 +429,6 @@
               (mt/rows
                 (qp/process-query query))))))))
 
-(deftest ^:parallel execute-multi-stage-metric
-  (let [mp (lib.metadata.jvm/application-database-metadata-provider (mt/id))
-        stage-one (-> (lib/query mp (lib.metadata/table mp (mt/id :orders)))
-                      (lib/breakout (lib/with-temporal-bucket
-                                      (lib.metadata/field mp (mt/id :orders :created_at))
-                                      :month))
-                      (lib/aggregate (lib/count))
-                      (lib/append-stage))
-        stage-one-cols (lib/visible-columns stage-one)
-        source-query (-> stage-one
-                         (lib/breakout (m/find-first (comp #{"Created At: Month"} :display-name) stage-one-cols))
-                         (lib/aggregate (lib/avg (m/find-first (comp #{"Count"} :display-name) stage-one-cols))))]
-    (mt/with-temp [:model/Card source-metric {:dataset_query (lib.convert/->legacy-MBQL source-query)
-                                              :database_id (mt/id)
-                                              :name "new_metric"
-                                              :type :metric}]
-      (let [query (lib/query mp (lib.metadata/card mp (:id source-metric)))]
-        (is (=? (mt/rows (qp/process-query source-query))
-                (mt/rows (qp/process-query query))))))))
-
 (deftest ^:parallel execute-single-stage-metric
   (let [mp (lib.metadata.jvm/application-database-metadata-provider (mt/id))
         source-query (-> (lib/query mp (lib.metadata/table mp (mt/id :products)))
@@ -539,24 +509,26 @@
 
 (deftest ^:parallel metric-with-nested-segments-test
   (let [mp (lib.tu/mock-metadata-provider
-             meta/metadata-provider
-             {:segments [{:id         1
-                          :name       "Segment 1"
-                          :table-id   (meta/id :venues)
-                          :definition {:filter [:= [:field (meta/id :venues :name) nil] "abc"]}}]})
+            meta/metadata-provider
+            {:segments [{:id         1
+                         :name       "Segment 1"
+                         :table-id   (meta/id :venues)
+                         :definition {:filter [:= [:field (meta/id :venues :name) nil] "abc"]}}]})
         [source-metric mp] (mock-metric mp (-> (basic-metric-query)
                                                (lib/filter (lib.metadata/segment mp 1))))]
     ;; Segments are handled further in the pipeline when the source is a metric
     (is (=?
-          {:stages [{:filters [[:segment {} 1]]}
-                    {}]}
-          (adjust (lib/query mp source-metric))))
+         {:stages
+          [{:source-table (meta/id :products)
+            :aggregation [[:avg {:name "Mock Metric"} [:field {} (meta/id :products :rating)]]]
+            :filters [[:= {} [:field {} (meta/id :venues :name)] some?]]}]}
+         (adjust (lib/query mp source-metric))))
     ;; Segments will be expanded in this case as the metric query that is spliced in needs to be processed
     (is (=?
-          {:stages [{:filters [[:= {} [:field {} (meta/id :venues :name)] some?]]}]}
-          (adjust
-            (-> (lib/query mp (meta/table-metadata :products))
-                (lib/aggregate (lib.metadata/metric mp (:id source-metric)))))))))
+         {:stages [{:filters [[:= {} [:field {} (meta/id :venues :name)] some?]]}]}
+         (adjust
+          (-> (lib/query mp (meta/table-metadata :products))
+              (lib/aggregate (lib.metadata/metric mp (:id source-metric)))))))))
 
 (deftest ^:parallel expand-macros-in-nested-queries-test
   (testing "expand-macros should expand things in the correct nested level (#12507)"
@@ -656,3 +628,27 @@
                          :filters [[:< {} [:field {} "RATING"] [:value {} 5]]
                                    [:> {} [:field {} "RATING"] [:value {} 3]]]}]}
               (adjust query))))))
+
+(deftest ^:parallel model-based-metric-with-implicit-join-test
+  (let [mp (lib.metadata.jvm/application-database-metadata-provider (mt/id))
+        model-query (lib/query mp (lib.metadata/table mp (mt/id :orders)))]
+    (mt/with-temp [:model/Card model {:dataset_query (lib.convert/->legacy-MBQL model-query)
+                                      :database_id (mt/id)
+                                      :name "Orders model"
+                                      :type :model}
+                   :model/Card metric {:dataset_query
+                                       (as-> (lib/query mp (lib.metadata/card mp (:id model))) $q
+                                         (lib/breakout $q (m/find-first (comp #{"Category"} :display-name)
+                                                                        (lib/breakoutable-columns $q)))
+                                         (lib/aggregate $q (lib/count))
+                                         (lib.convert/->legacy-MBQL $q))
+                                       :database_id (mt/id)
+                                       :name "Orders model metric"
+                                       :type :metric}]
+      (let [metric-query (lib/query mp (lib.metadata/card mp (:id metric)))
+            etalon-query (as-> (lib/query mp (lib.metadata/card mp (:id model))) $q
+                           (lib/breakout $q (m/find-first (comp #{"Category"} :display-name)
+                                                          (lib/breakoutable-columns $q)))
+                           (lib/aggregate $q (lib/count)))]
+        (is (=? (mt/rows (qp/process-query etalon-query))
+                (mt/rows (qp/process-query metric-query))))))))
-- 
GitLab