From 3a9d45a964c5a9d0f20d29f11aa3cf58756155d1 Mon Sep 17 00:00:00 2001
From: Tom Robinson <tlrobinson@gmail.com>
Date: Wed, 18 Mar 2020 10:17:37 -0700
Subject: [PATCH] Finish consolidating column to field ref functions (#12095)

* Finish consolidating column => field ref functions

* Add special case and tests for keyForColumn with joined-field
---
 .../lib/queries/StructuredQuery.js            |  34 +---
 frontend/src/metabase/lib/dataset.js          |  89 ++---------
 .../drill/SummarizeColumnByTimeDrill.js       |   4 +-
 .../components/drill/SummarizeColumnDrill.js  |   4 +-
 frontend/src/metabase/modes/lib/actions.js    |  73 ++-------
 frontend/src/metabase/modes/lib/drilldown.js  |   4 +-
 .../components/TableInteractive.jsx           |   2 +-
 .../lib/queries/StructuredQuery.unit.spec.js  |   2 +-
 .../test/metabase/lib/dataset.unit.spec.js    | 148 ++++++++++++------
 9 files changed, 134 insertions(+), 226 deletions(-)

diff --git a/frontend/src/metabase-lib/lib/queries/StructuredQuery.js b/frontend/src/metabase-lib/lib/queries/StructuredQuery.js
index aaaabcd061b..ba0cab01d1e 100644
--- a/frontend/src/metabase-lib/lib/queries/StructuredQuery.js
+++ b/frontend/src/metabase-lib/lib/queries/StructuredQuery.js
@@ -64,7 +64,7 @@ import { augmentDatabase } from "metabase/lib/table";
 import { TYPE } from "metabase/lib/types";
 
 import { isSegmentFilter } from "metabase/lib/query/filter";
-import { fieldRefForColumnWithLegacyFallback } from "metabase/lib/dataset";
+import { fieldRefForColumn } from "metabase/lib/dataset";
 
 type DimensionFilter = (dimension: Dimension) => boolean;
 type FieldFilter = (filter: Field) => boolean;
@@ -1324,37 +1324,7 @@ export default class StructuredQuery extends AtomicQuery {
   }
 
   fieldReferenceForColumn(column) {
-    return fieldRefForColumnWithLegacyFallback(
-      column,
-      c => this.fieldReferenceForColumn_LEGACY(c),
-      "StructuredQuery::fieldReferenceForColumn",
-      ["field-literal"],
-    );
-  }
-
-  // LEGACY:
-  fieldReferenceForColumn_LEGACY(column) {
-    if (column.fk_field_id != null) {
-      return [
-        "fk->",
-        ["field-id", column.fk_field_id],
-        ["field-id", column.id],
-      ];
-    } else if (column.id != null) {
-      return ["field-id", column.id];
-    } else if (column.expression_name != null) {
-      return ["expression", column.expression_name];
-    } else if (column.source === "aggregation") {
-      // HACK: ideally column would include the aggregation index directly
-      const columnIndex = _.findIndex(
-        this.columnNames(),
-        name => name === column.name,
-      );
-      if (columnIndex >= 0) {
-        return this.columnDimensions()[columnIndex].mbql();
-      }
-    }
-    return null;
+    return fieldRefForColumn(column);
   }
 
   // TODO: better name may be parseDimension?
diff --git a/frontend/src/metabase/lib/dataset.js b/frontend/src/metabase/lib/dataset.js
index d088b60a846..415dd15eed5 100644
--- a/frontend/src/metabase/lib/dataset.js
+++ b/frontend/src/metabase/lib/dataset.js
@@ -41,95 +41,28 @@ export const rangeForValue = (
   }
 };
 
-const loggedKeys = new Set(); // just to make sure we log each mismatch only once
-export function fieldRefForColumnWithLegacyFallback(
-  column: any,
-  fieldRefForColumn_LEGACY: any,
-  debugName: any,
-  whitelist?: string[],
-): any {
-  // NOTE: matching existing behavior of returning the unwrapped base dimension until we understand the implications of changing this
-  const fieldRef =
-    column.field_ref &&
-    Dimension.parseMBQL(column.field_ref)
-      .baseDimension()
-      .mbql();
-
-  // TODO: remove this once we're sure field_ref is returning correct values
-  const fieldRef_LEGACY =
-    fieldRefForColumn_LEGACY && fieldRefForColumn_LEGACY(column);
-
-  const key = JSON.stringify([debugName, fieldRef, fieldRef_LEGACY]);
-  if (fieldRefForColumn_LEGACY && !loggedKeys.has(key)) {
-    loggedKeys.add(key);
-    if (!_.isEqual(fieldRef, fieldRef_LEGACY)) {
-      console.group(debugName + " mismatch");
-      console.warn("column", column.name, column.field_ref);
-      console.warn("new", fieldRef);
-      console.warn("old", fieldRef_LEGACY);
-      console.groupEnd();
-    }
-  }
-
-  // NOTE: whitelisting known correct clauses for now while we make sure the rest are correct
-  if (!whitelist || (fieldRef && whitelist.includes(fieldRef[0]))) {
-    return fieldRef;
-  }
-  return fieldRef_LEGACY;
-}
-
 /**
  * Returns a MBQL field reference (FieldReference) for a given result dataset column
  *
  * @param  {Column} column Dataset result column
- * @param  {?Column[]} columns Full array of columns, unfortunately needed to determine the aggregation index
  * @return {?FieldReference} MBQL field reference
  */
-export function fieldRefForColumn(
-  column: Column,
-  columns?: Column[],
-): ?FieldReference {
-  return fieldRefForColumnWithLegacyFallback(
-    column,
-    c => fieldRefForColumn_LEGACY(c, columns),
-    "dataset::fieldRefForColumn",
-    ["field-literal"],
+export function fieldRefForColumn(column: Column): ?FieldReference {
+  // NOTE: matching existing behavior of returning the unwrapped base dimension until we understand the implications of changing this
+  return (
+    column.field_ref &&
+    Dimension.parseMBQL(column.field_ref)
+      .baseDimension()
+      .mbql()
   );
 }
 
-function fieldRefForColumn_LEGACY(
-  column: Column,
-  columns?: Column[],
-): ?FieldReference {
-  if (column.id != null) {
-    if (Array.isArray(column.id)) {
-      // $FlowFixMe: sometimes col.id is a field reference (e.x. nested queries), if so just return it
-      return column.id;
-    } else if (column.fk_field_id != null) {
-      return [
-        "fk->",
-        ["field-id", column.fk_field_id],
-        ["field-id", column.id],
-      ];
-    } else {
-      return ["field-id", column.id];
-    }
-  } else if (column.expression_name != null) {
-    return ["expression", column.expression_name];
-  } else if (column.source === "aggregation" && columns) {
-    // HACK: find the aggregation index, preferably this would be included on the column
-    const aggIndex = columns
-      .filter(c => c.source === "aggregation")
-      .indexOf(column);
-    if (aggIndex >= 0) {
-      return ["aggregation", aggIndex];
-    }
-  }
-  return null;
-}
-
 export const keyForColumn = (column: Column): string => {
   const ref = fieldRefForColumn(column);
+  // match bug where joined-field returned field-id instead
+  if (Array.isArray(ref) && ref[0] === "joined-field") {
+    return JSON.stringify(["ref", ref[2]]);
+  }
   // match legacy behavior which didn't have "field-literal" or "aggregation" field refs
   if (
     Array.isArray(ref) &&
diff --git a/frontend/src/metabase/modes/components/drill/SummarizeColumnByTimeDrill.js b/frontend/src/metabase/modes/components/drill/SummarizeColumnByTimeDrill.js
index 34bcfd0388a..5f7562b83a0 100644
--- a/frontend/src/metabase/modes/components/drill/SummarizeColumnByTimeDrill.js
+++ b/frontend/src/metabase/modes/components/drill/SummarizeColumnByTimeDrill.js
@@ -3,7 +3,7 @@
 import React from "react";
 import { t } from "ttag";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
-import { getFieldRefFromColumn } from "metabase/modes/lib/actions";
+import { fieldRefForColumn } from "metabase/lib/dataset";
 import {
   getAggregationOperator,
   isCompatibleAggregationOperatorForField,
@@ -47,7 +47,7 @@ export default ({
         question
           .aggregate(
             aggregator.requiresField
-              ? [aggregator.short, getFieldRefFromColumn(column)]
+              ? [aggregator.short, fieldRefForColumn(column)]
               : [aggregator.short],
           )
           .pivot([dateDimension.defaultDimension().mbql()]),
diff --git a/frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.js b/frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.js
index b4516a9cb30..a810c00ea5e 100644
--- a/frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.js
+++ b/frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.js
@@ -1,6 +1,6 @@
 /* @flow */
 
-import { getFieldRefFromColumn } from "metabase/modes/lib/actions";
+import { fieldRefForColumn } from "metabase/lib/dataset";
 import {
   getAggregationOperator,
   isCompatibleAggregationOperatorForField,
@@ -66,7 +66,7 @@ export default ({
       question: () =>
         query
           // $FlowFixMe
-          .aggregate([aggregator.short, getFieldRefFromColumn(column)])
+          .aggregate([aggregator.short, fieldRefForColumn(column)])
           .question()
           .setDefaultDisplay(),
       action: () => dispatch => {
diff --git a/frontend/src/metabase/modes/lib/actions.js b/frontend/src/metabase/modes/lib/actions.js
index 795f4a7d1ee..21ddc407481 100644
--- a/frontend/src/metabase/modes/lib/actions.js
+++ b/frontend/src/metabase/modes/lib/actions.js
@@ -2,19 +2,10 @@
 
 import moment from "moment";
 
-import {
-  rangeForValue,
-  fieldRefForColumnWithLegacyFallback,
-} from "metabase/lib/dataset";
+import { rangeForValue, fieldRefForColumn } from "metabase/lib/dataset";
 import { isDate, isNumber } from "metabase/lib/schema_metadata";
 
-import type {
-  Breakout,
-  LocalFieldReference,
-  ForeignFieldReference,
-  FieldLiteral,
-} from "metabase/meta/types/Query";
-import type { Column } from "metabase/meta/types/Dataset";
+import type { Breakout } from "metabase/meta/types/Query";
 import type { DimensionValue } from "metabase/meta/types/Visualization";
 import { parseTimestamp } from "metabase/lib/time";
 
@@ -50,7 +41,7 @@ export function filter(question: Question, operator, column, value): ?Question {
   const query = question.query();
   if (query instanceof StructuredQuery) {
     return query
-      .filter([operator, getFieldRefFromColumn(column), value])
+      .filter([operator, fieldRefForColumn(column), value])
       .question();
   }
 }
@@ -64,7 +55,7 @@ export function pivot(
   if (query instanceof StructuredQuery) {
     for (const dimension of dimensions) {
       query = drillFilter(query, dimension.value, dimension.column);
-      const dimensionRef = getFieldRefFromColumn(dimension.column);
+      const dimensionRef = fieldRefForColumn(dimension.column);
       for (let i = 0; i < query.breakouts().length; i++) {
         const breakout = query.breakouts()[i];
         if (breakout.dimension().isSameBaseDimension(dimensionRef)) {
@@ -84,10 +75,10 @@ export function distribution(question: Question, column): ?Question {
   const query = question.query();
   if (query instanceof StructuredQuery) {
     const breakout = isDate(column)
-      ? ["datetime-field", getFieldRefFromColumn(column), "month"]
+      ? ["datetime-field", fieldRefForColumn(column), "month"]
       : isNumber(column)
-      ? ["binning-strategy", getFieldRefFromColumn(column), "default"]
-      : getFieldRefFromColumn(column);
+      ? ["binning-strategy", fieldRefForColumn(column), "default"]
+      : fieldRefForColumn(column);
     return query
       .clearAggregations()
       .clearBreakouts()
@@ -138,15 +129,15 @@ export function drillFilter(
   if (isDate(column)) {
     filter = [
       "=",
-      ["datetime-field", getFieldRefFromColumn(column), column.unit],
+      ["datetime-field", fieldRefForColumn(column), column.unit],
       parseTimestamp(value, column.unit).format(),
     ];
   } else {
     const range = rangeForValue(value, column);
     if (range) {
-      filter = ["between", getFieldRefFromColumn(column), range[0], range[1]];
+      filter = ["between", fieldRefForColumn(column), range[0], range[1]];
     } else {
-      filter = ["=", getFieldRefFromColumn(column), value];
+      filter = ["=", fieldRefForColumn(column), value];
     }
   }
 
@@ -196,7 +187,7 @@ export function updateDateTimeFilter(
   start,
   end,
 ): StructuredQuery {
-  const fieldRef = getFieldRefFromColumn(column);
+  const fieldRef = fieldRefForColumn(column);
   start = moment(start);
   end = moment(end);
   if (column.unit) {
@@ -259,8 +250,8 @@ export function updateLatLonFilter(
 ): StructuredQuery {
   return addOrUpdateFilter(query, [
     "inside",
-    getFieldRefFromColumn(latitudeColumn),
-    getFieldRefFromColumn(longitudeColumn),
+    fieldRefForColumn(latitudeColumn),
+    fieldRefForColumn(longitudeColumn),
     bounds.getNorth(),
     bounds.getWest(),
     bounds.getSouth(),
@@ -274,42 +265,6 @@ export function updateNumericFilter(
   start,
   end,
 ): StructuredQuery {
-  const fieldRef = getFieldRefFromColumn(column);
+  const fieldRef = fieldRefForColumn(column);
   return addOrUpdateFilter(query, ["between", fieldRef, start, end]);
 }
-
-// COLUMN UTILITIES
-
-export function getFieldRefFromColumn(
-  column: Column,
-): LocalFieldReference | ForeignFieldReference | FieldLiteral {
-  return fieldRefForColumnWithLegacyFallback(
-    column,
-    c => getFieldRefFromColumn_LEGACY(c),
-    "actions::getFieldRefFromColumn",
-  );
-}
-
-function getFieldRefFromColumn_LEGACY(
-  column: Column,
-): LocalFieldReference | ForeignFieldReference | FieldLiteral {
-  if (column.expression_name) {
-    return ["expression", column.expression_name];
-  }
-
-  const fieldId = column.id;
-  if (fieldId == null) {
-    return null;
-    // throw new Error(
-    //   "getFieldRefFromColumn expects non-null fieldId or column with non-null id",
-    // );
-  }
-  if (Array.isArray(fieldId)) {
-    // NOTE: sometimes col.id is a field reference (e.x. nested queries), if so just return it
-    return fieldId;
-  } else if (column.fk_field_id != null) {
-    return ["fk->", ["field-id", column.fk_field_id], ["field-id", fieldId]];
-  } else {
-    return ["field-id", fieldId];
-  }
-}
diff --git a/frontend/src/metabase/modes/lib/drilldown.js b/frontend/src/metabase/modes/lib/drilldown.js
index b711a895952..8fef2fc48b4 100644
--- a/frontend/src/metabase/modes/lib/drilldown.js
+++ b/frontend/src/metabase/modes/lib/drilldown.js
@@ -7,7 +7,7 @@ import {
   isDate,
   isAny,
 } from "metabase/lib/schema_metadata";
-import { getFieldRefFromColumn } from "./actions";
+import { fieldRefForColumn } from "metabase/lib/dataset";
 
 import _ from "underscore";
 import { getIn } from "icepick";
@@ -172,7 +172,7 @@ function breakoutForBreakoutTemplate(breakoutTemplate, dimensions, table) {
     return null;
   }
 
-  let fieldRef = getFieldRefFromColumn(dimensions[0].column);
+  let fieldRef = fieldRefForColumn(dimensions[0].column);
 
   if (Array.isArray(fieldRef) && fieldRef[0] === "field-id") {
     fieldRef = ["field-id", field.id];
diff --git a/frontend/src/metabase/visualizations/components/TableInteractive.jsx b/frontend/src/metabase/visualizations/components/TableInteractive.jsx
index 9f9ab8b347d..a5c47826d5d 100644
--- a/frontend/src/metabase/visualizations/components/TableInteractive.jsx
+++ b/frontend/src/metabase/visualizations/components/TableInteractive.jsx
@@ -594,7 +594,7 @@ export default class TableInteractive extends Component {
     const isRightAligned = isColumnRightAligned(column);
 
     // TODO MBQL: use query lib to get the sort field
-    const fieldRef = fieldRefForColumn(column, cols);
+    const fieldRef = fieldRefForColumn(column);
     const sortIndex = _.findIndex(
       sort,
       sort => sort[1] && Dimension.isEqual(sort[1], fieldRef),
diff --git a/frontend/test/metabase-lib/lib/queries/StructuredQuery.unit.spec.js b/frontend/test/metabase-lib/lib/queries/StructuredQuery.unit.spec.js
index b51e7af36e5..28d5b352a99 100644
--- a/frontend/test/metabase-lib/lib/queries/StructuredQuery.unit.spec.js
+++ b/frontend/test/metabase-lib/lib/queries/StructuredQuery.unit.spec.js
@@ -563,7 +563,7 @@ describe("StructuredQuery", () => {
 
   describe("FIELD REFERENCE METHODS", () => {
     describe("fieldReferenceForColumn", () => {
-      it('should return `["field-id", 1]` for a normal column', () => {
+      xit('should return `["field-id", 1]` for a normal column', () => {
         expect(query.fieldReferenceForColumn({ id: ORDERS.TOTAL.id })).toEqual([
           "field-id",
           ORDERS.TOTAL.id,
diff --git a/frontend/test/metabase/lib/dataset.unit.spec.js b/frontend/test/metabase/lib/dataset.unit.spec.js
index 9bdb26737ce..ce869725b37 100644
--- a/frontend/test/metabase/lib/dataset.unit.spec.js
+++ b/frontend/test/metabase/lib/dataset.unit.spec.js
@@ -2,59 +2,17 @@ import {
   fieldRefForColumn,
   syncTableColumnsToQuery,
   findColumnForColumnSetting,
+  keyForColumn,
 } from "metabase/lib/dataset";
 
 import { ORDERS, PRODUCTS } from "__support__/sample_dataset_fixture";
 
-const FIELD_COLUMN = { id: 1 };
-const FK_COLUMN = { id: 1, fk_field_id: 2 };
-const EXPRESSION_COLUMN = { expression_name: "foo" };
-const AGGREGATION_COLUMN = { source: "aggregation" };
-
 describe("metabase/util/dataset", () => {
   describe("fieldRefForColumn", () => {
-    it('should return `["field-id", 1]` for a normal column', () => {
-      expect(fieldRefForColumn(FIELD_COLUMN)).toEqual(["field-id", 1]);
-    });
-    it('should return `["fk->", ["field-id", 2], ["field-id", 1]]` for a fk column', () => {
-      expect(fieldRefForColumn(FK_COLUMN)).toEqual([
-        "fk->",
-        ["field-id", 2],
-        ["field-id", 1],
-      ]);
-    });
-    it('should return `["expression", 2, 1]` for a fk column', () => {
-      expect(fieldRefForColumn(EXPRESSION_COLUMN)).toEqual([
-        "expression",
-        "foo",
-      ]);
-    });
-
-    describe("aggregation column", () => {
-      // this is an unfortunate effect of the backend not returning enough information to determine the aggregation index from the column
-      it("should return `null` for aggregation column if list of columns was provided", () => {
-        expect(fieldRefForColumn(AGGREGATION_COLUMN)).toEqual(null);
-      });
-      it('should return `["aggregation", 0]` for aggregation column if list of columns was provided', () => {
-        expect(
-          fieldRefForColumn(AGGREGATION_COLUMN, [AGGREGATION_COLUMN]),
-        ).toEqual(["aggregation", 0]);
-      });
-      it('should return `["aggregation", 1]` for second aggregation column if list of columns was provided', () => {
-        expect(
-          fieldRefForColumn(AGGREGATION_COLUMN, [
-            { source: "aggregation" },
-            AGGREGATION_COLUMN,
-          ]),
-        ).toEqual(["aggregation", 1]);
-      });
-    });
-
-    // NOTE: sometimes id is an MBQL clause itself, e.x. nested queries
-    it("should return `id` if is an MBQL clause", () => {
-      expect(fieldRefForColumn({ id: ["field-id", 3] })).toEqual([
+    it("should return field_ref from the column", () => {
+      expect(fieldRefForColumn({ field_ref: ["field-id", 42] })).toEqual([
         "field-id",
-        3,
+        42,
       ]);
     });
   });
@@ -204,9 +162,9 @@ describe("metabase/util/dataset", () => {
 
   describe("findColumnForColumnSetting", () => {
     const columns = [
-      { name: "bar", id: 42 },
-      { name: "foo", id: 1, fk_field_id: 2 },
-      { name: "baz", id: 43 },
+      { name: "bar", field_ref: ["field-id", 42] },
+      { name: "foo", field_ref: ["fk->", ["field-id", 2], ["field-id", 1]] },
+      { name: "baz", field_ref: ["field-id", 43] },
     ];
     it("should find column with name", () => {
       const column = findColumnForColumnSetting(columns, { name: "foo" });
@@ -225,4 +183,96 @@ describe("metabase/util/dataset", () => {
       expect(column).toBe(columns[1]);
     });
   });
+
+  describe("keyForColumn", () => {
+    // NOTE: run legacy tests with and without a field_ref. without is disabled in latest since it now always uses
+    // field_ref, leaving test code in place to compare against older versions
+    for (const fieldRefEnabled of [/*false,*/ true]) {
+      describe(fieldRefEnabled ? "with field_ref" : "without field_ref", () => {
+        it("should return [ref [field-id ...]] for field", () => {
+          expect(
+            keyForColumn({
+              name: "foo",
+              id: 1,
+              field_ref: fieldRefEnabled ? ["field-id", 1] : undefined,
+            }),
+          ).toEqual(JSON.stringify(["ref", ["field-id", 1]]));
+        });
+        it("should return [ref [fk-> ...]] for foreign field", () => {
+          expect(
+            keyForColumn({
+              name: "foo",
+              id: 1,
+              fk_field_id: 2,
+              field_ref: fieldRefEnabled
+                ? ["fk->", ["field-id", 2], ["field-id", 1]]
+                : undefined,
+            }),
+          ).toEqual(
+            JSON.stringify(["ref", ["fk->", ["field-id", 2], ["field-id", 1]]]),
+          );
+        });
+        it("should return [ref [expression ...]] for expression", () => {
+          expect(
+            keyForColumn({
+              name: "foo",
+              expression_name: "foo",
+              field_ref: fieldRefEnabled ? ["expression", "foo"] : undefined,
+            }),
+          ).toEqual(JSON.stringify(["ref", ["expression", "foo"]]));
+        });
+        it("should return [name ...] for aggregation", () => {
+          const col = {
+            name: "foo",
+            source: "aggregation",
+            field_ref: fieldRefEnabled ? ["aggregation", 0] : undefined,
+          };
+          expect(keyForColumn(col, [col])).toEqual(
+            // NOTE: not ideal, matches existing behavior, but should be ["aggregation", 0]
+            JSON.stringify(["name", "foo"]),
+          );
+        });
+        it("should return [name ...] for field-literal", () => {
+          const col = {
+            name: "foo",
+            id: ["field-literal", "foo", "type/Integer"],
+            field_ref: fieldRefEnabled
+              ? ["field-literal", "foo", "type/Integer"]
+              : undefined,
+          };
+          expect(keyForColumn(col, [col])).toEqual(
+            // NOTE: not ideal, matches existing behavior, but should be ["field-literal", "foo", "type/Integer"]
+            JSON.stringify(["name", "foo"]),
+          );
+        });
+        it("should return [name ...] for native query column", () => {
+          expect(
+            keyForColumn({
+              name: "foo",
+              field_ref: fieldRefEnabled
+                ? ["field-literal", "foo", "type/Integer"]
+                : undefined,
+            }),
+          ).toEqual(
+            // NOTE: not ideal, matches existing behavior, but should be ["field-literal", "foo", "type/Integer"]
+            JSON.stringify(["name", "foo"]),
+          );
+        });
+      });
+    }
+
+    describe("with field_ref", () => {
+      it("should return [ref [field-id ...]] for joined-field", () => {
+        const col = {
+          name: "foo",
+          id: 1,
+          field_ref: ["joined-field", "x", ["field-id", 1]],
+        };
+        expect(keyForColumn(col)).toEqual(
+          // NOTE: not ideal, matches existing behavior, but should be ["joined-field", "x", ["field-id", 1]]
+          JSON.stringify(["ref", ["field-id", 1]]),
+        );
+      });
+    });
+  });
 });
-- 
GitLab