From d327c451eadbc873b70ca42a007afd0e5b78747c Mon Sep 17 00:00:00 2001
From: Aleksandr Lesnenko <alxnddr@users.noreply.github.com>
Date: Mon, 17 Oct 2022 16:03:16 +0400
Subject: [PATCH] untie `getColumnKey` function from code not usable in static
 viz rendering environment (#25925)

* untie getColumnKey function from unimportable in static viz environment code

* review

* spec
---
 frontend/src/metabase-lib/lib/Dimension.ts    |  77 +++-------
 frontend/src/metabase-lib/lib/Question.ts     |   5 +-
 .../metabase-lib/lib/parameters/utils/mbql.js |   9 +-
 .../metabase-lib/lib/queries/NativeQuery.ts   |   5 +-
 .../metabase-lib/lib/queries/utils/dataset.js |  35 +----
 .../lib/queries/utils/get-column-key.ts       |  50 +++++++
 .../queries/utils/get-column-key.unit.spec.js |  91 ++++++++++++
 frontend/src/metabase-lib/lib/references.ts   | 124 +++++++++++++++++
 .../metabase-lib/lib/references.unit.spec.ts  | 131 ++++++++++++++++++
 frontend/src/metabase-types/api/dataset.ts    |   8 +-
 frontend/src/metabase-types/api/query.ts      |  67 +++++++++
 .../ClickBehaviorSidebar.tsx                  |  10 +-
 .../ClickBehaviorSidebarContent.tsx           |   6 +-
 .../ClickBehaviorSidebarHeader.tsx            |   5 +-
 .../TableClickBehaviorView.tsx                |  10 +-
 .../components/ClickBehaviorSidebar/utils.ts  |   8 +-
 .../modes/components/drill/FormatAction.jsx   |   4 +-
 .../filters/pickers/DefaultPicker.jsx         |   4 +-
 .../components/RowChart/utils/format.ts       |   5 +-
 .../settings/ChartSettingFieldPicker.jsx      |   4 +-
 .../settings/ChartSettingFieldsPartition.jsx  |   4 +-
 .../settings/ChartSettingOrderedColumns.jsx   |   8 +-
 .../ChartSettingsListColumns.tsx              |   8 +-
 .../visualizations/lib/settings/column.js     |   4 +-
 .../metabase-lib/lib/Dimension.unit.spec.js   |  61 --------
 .../test/metabase/lib/dataset.unit.spec.js    |  93 -------------
 26 files changed, 540 insertions(+), 296 deletions(-)
 create mode 100644 frontend/src/metabase-lib/lib/queries/utils/get-column-key.ts
 create mode 100644 frontend/src/metabase-lib/lib/queries/utils/get-column-key.unit.spec.js
 create mode 100644 frontend/src/metabase-lib/lib/references.ts
 create mode 100644 frontend/src/metabase-lib/lib/references.unit.spec.ts

diff --git a/frontend/src/metabase-lib/lib/Dimension.ts b/frontend/src/metabase-lib/lib/Dimension.ts
index 8e981b97351..acc9b23089d 100644
--- a/frontend/src/metabase-lib/lib/Dimension.ts
+++ b/frontend/src/metabase-lib/lib/Dimension.ts
@@ -34,6 +34,15 @@ import Aggregation from "metabase-lib/lib/queries/structured/Aggregation";
 import Filter from "metabase-lib/lib/queries/structured/Filter";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 import NativeQuery from "metabase-lib/lib/queries/NativeQuery";
+import {
+  isFieldReference,
+  isExpressionReference,
+  isAggregationReference,
+  isTemplateTagReference,
+  normalizeReferenceOptions,
+  getBaseDimensionReference,
+  BASE_DIMENSION_REFERENCE_OMIT_OPTIONS,
+} from "metabase-lib/lib/references";
 
 /**
  * A dimension option returned by the query_metadata API
@@ -87,23 +96,6 @@ export default class Dimension {
     this._options = options;
   }
 
-  /**
-   * Canonically the field clause should use `null` instead of empty options. Keys with null values should get removed.
-   */
-  static normalizeOptions(options: any): any {
-    if (!options) {
-      return null;
-    }
-
-    // recursively normalize maps inside options.
-    options = _.mapObject(options, val =>
-      typeof val === "object" ? this.normalizeOptions(val) : val,
-    );
-    // remove null/undefined options from map.
-    options = _.omit(options, value => value == null);
-    return _.isEmpty(options) ? null : options;
-  }
-
   /**
    * Parses an MBQL expression into an appropriate Dimension subclass, if possible.
    * Metadata should be provided if you intend to use the display name or render methods.
@@ -219,7 +211,7 @@ export default class Dimension {
    */
   _dimensionForOption(option: DimensionOption) {
     // fill in the parent field ref
-    const fieldRef = this.baseDimension().mbql();
+    const fieldRef = getBaseDimensionReference(this.mbql());
     let mbql = option.mbql;
 
     if (mbql) {
@@ -565,13 +557,6 @@ export default class Dimension {
     return this.withoutOptions("temporal-unit");
   }
 
-  /**
-   * Return a copy of this Dimension with any binning options removed.
-   */
-  withoutBinning(): Dimension {
-    return this.withoutOptions("binning");
-  }
-
   withoutJoinAlias(): Dimension {
     return this.withoutOptions("join-alias");
   }
@@ -580,7 +565,7 @@ export default class Dimension {
    * Return a copy of this Dimension with any temporal bucketing or binning options removed.
    */
   baseDimension(): Dimension {
-    return this.withoutTemporalBucketing().withoutBinning();
+    return this.withoutOptions(...BASE_DIMENSION_REFERENCE_OMIT_OPTIONS);
   }
 
   isValidFKRemappingTarget() {
@@ -675,21 +660,12 @@ export default class Dimension {
  */
 
 export class FieldDimension extends Dimension {
-  /**
-   * Whether `clause` is an array, and a valid `:field` clause
-   */
-  static isFieldClause(clause): boolean {
-    return (
-      Array.isArray(clause) && clause.length === 3 && clause[0] === "field"
-    );
-  }
-
   static parseMBQL(
     mbql,
     metadata = null,
     query = null,
   ): FieldDimension | null | undefined {
-    if (FieldDimension.isFieldClause(mbql)) {
+    if (isFieldReference(mbql)) {
       return Object.freeze(
         new FieldDimension(mbql[1], mbql[2], metadata, query),
       );
@@ -742,7 +718,7 @@ export class FieldDimension extends Dimension {
       [fieldIdOrName, options],
       metadata,
       query,
-      Object.freeze(Dimension.normalizeOptions(options)),
+      Object.freeze(normalizeReferenceOptions(options)),
     );
     this._fieldIdOrName = fieldIdOrName;
 
@@ -764,7 +740,7 @@ export class FieldDimension extends Dimension {
     }
 
     // this should be considered equivalent to an equivalent MBQL clause
-    if (FieldDimension.isFieldClause(somethingElse)) {
+    if (isFieldReference(somethingElse)) {
       const dimension = FieldDimension.parseMBQL(
         somethingElse,
         this._metadata,
@@ -1142,21 +1118,12 @@ const isFieldDimension = dimension => dimension instanceof FieldDimension;
 export class ExpressionDimension extends Dimension {
   _expressionName: ExpressionName;
 
-  /**
-   * Whether `clause` is an array, and a valid `:expression` clause
-   */
-  static isExpressionClause(clause): boolean {
-    return (
-      Array.isArray(clause) && clause.length >= 2 && clause[0] === "expression"
-    );
-  }
-
   static parseMBQL(
     mbql: any,
     metadata?: Metadata | null | undefined,
     query?: StructuredQuery | null | undefined,
   ): Dimension | null | undefined {
-    if (ExpressionDimension.isExpressionClause(mbql)) {
+    if (isExpressionReference(mbql)) {
       const [expressionName, options] = mbql.slice(1);
       return new ExpressionDimension(expressionName, options, metadata, query);
     }
@@ -1174,7 +1141,7 @@ export class ExpressionDimension extends Dimension {
       [expressionName, options],
       metadata,
       query,
-      Object.freeze(Dimension.normalizeOptions(options)),
+      Object.freeze(normalizeReferenceOptions(options)),
     );
     this._expressionName = expressionName;
 
@@ -1195,7 +1162,7 @@ export class ExpressionDimension extends Dimension {
       );
     }
 
-    if (ExpressionDimension.isExpressionClause(somethingElse)) {
+    if (isExpressionReference(somethingElse)) {
       const dimension = ExpressionDimension.parseMBQL(
         somethingElse,
         this._metadata,
@@ -1411,7 +1378,7 @@ export class AggregationDimension extends Dimension {
     metadata?: Metadata | null | undefined,
     query?: StructuredQuery | null | undefined,
   ): Dimension | null | undefined {
-    if (Array.isArray(mbql) && mbql[0] === "aggregation") {
+    if (isAggregationReference(mbql)) {
       const [aggregationIndex, options] = mbql.slice(1);
       return new AggregationDimension(
         aggregationIndex,
@@ -1434,7 +1401,7 @@ export class AggregationDimension extends Dimension {
       [aggregationIndex, options],
       metadata,
       query,
-      Object.freeze(Dimension.normalizeOptions(options)),
+      Object.freeze(normalizeReferenceOptions(options)),
     );
     this._aggregationIndex = aggregationIndex;
 
@@ -1542,15 +1509,11 @@ export class TemplateTagDimension extends FieldDimension {
     metadata: Metadata = null,
     query: NativeQuery = null,
   ): FieldDimension | null | undefined {
-    return TemplateTagDimension.isTemplateTagClause(mbql)
+    return isTemplateTagReference(mbql)
       ? Object.freeze(new TemplateTagDimension(mbql[1], metadata, query))
       : null;
   }
 
-  static isTemplateTagClause(clause) {
-    return Array.isArray(clause) && clause[0] === "template-tag";
-  }
-
   validateTemplateTag(): ValidationError | null {
     const tag = this.tag();
     if (!tag) {
diff --git a/frontend/src/metabase-lib/lib/Question.ts b/frontend/src/metabase-lib/lib/Question.ts
index b705be49f10..3ccfd722f20 100644
--- a/frontend/src/metabase-lib/lib/Question.ts
+++ b/frontend/src/metabase-lib/lib/Question.ts
@@ -22,8 +22,6 @@ import {
   AggregationDimension,
   FieldDimension,
 } from "metabase-lib/lib/Dimension";
-import Mode from "metabase-lib/lib/Mode";
-import { isStandard } from "metabase-lib/lib/queries/utils/filter";
 import { isFK } from "metabase-lib/lib/types/utils/isa";
 import { memoizeClass, sortObject } from "metabase-lib/lib/utils";
 
@@ -81,6 +79,7 @@ import {
   ALERT_TYPE_ROWS,
   ALERT_TYPE_TIMESERIES_GOAL,
 } from "metabase-lib/lib/Alert";
+import { getBaseDimensionReference } from "metabase-lib/lib/references";
 
 export type QuestionCreatorOpts = {
   databaseId?: DatabaseId;
@@ -713,7 +712,7 @@ class QuestionInner {
             const dimension = query.columnDimensionWithName(name);
             return {
               name: name,
-              field_ref: dimension.baseDimension().mbql(),
+              field_ref: getBaseDimensionReference(dimension.mbql()),
               enabled: true,
             };
           }),
diff --git a/frontend/src/metabase-lib/lib/parameters/utils/mbql.js b/frontend/src/metabase-lib/lib/parameters/utils/mbql.js
index 63d8c7fc6ab..f8cd68de093 100644
--- a/frontend/src/metabase-lib/lib/parameters/utils/mbql.js
+++ b/frontend/src/metabase-lib/lib/parameters/utils/mbql.js
@@ -6,15 +6,13 @@ import {
   EXCLUDE_OPTIONS,
   EXCLUDE_UNITS,
 } from "metabase-lib/lib/queries/utils/query-time";
-import Dimension, {
-  FieldDimension,
-  TemplateTagDimension,
-} from "metabase-lib/lib/Dimension";
+import Dimension, { FieldDimension } from "metabase-lib/lib/Dimension";
 import { isDimensionTarget } from "metabase-lib/lib/parameters/utils/targets";
 import {
   getParameterSubType,
   isDateParameter,
 } from "metabase-lib/lib/parameters/utils/parameter-type";
+import { isTemplateTagReference } from "metabase-lib/lib/references";
 import { getParameterOperatorName } from "metabase-lib/lib/parameters/utils/operators";
 import { hasParameterValue } from "metabase-lib/lib/parameters/utils/parameter-values";
 
@@ -168,8 +166,7 @@ export function isFieldFilterParameterConveratableToMBQL(parameter) {
   const hasValue = hasParameterValue(value);
   const hasWellFormedTarget = Array.isArray(target?.[1]);
   const hasFieldDimensionTarget =
-    isDimensionTarget(target) &&
-    !TemplateTagDimension.isTemplateTagClause(target[1]);
+    isDimensionTarget(target) && !isTemplateTagReference(target[1]);
 
   return hasValue && hasWellFormedTarget && hasFieldDimensionTarget;
 }
diff --git a/frontend/src/metabase-lib/lib/queries/NativeQuery.ts b/frontend/src/metabase-lib/lib/queries/NativeQuery.ts
index bcc24093b60..ac382b2898a 100644
--- a/frontend/src/metabase-lib/lib/queries/NativeQuery.ts
+++ b/frontend/src/metabase-lib/lib/queries/NativeQuery.ts
@@ -25,6 +25,7 @@ import Variable from "metabase-lib/lib/variables/Variable";
 import TemplateTagVariable from "metabase-lib/lib/variables/TemplateTagVariable";
 import { createTemplateTag } from "metabase-lib/lib/queries/TemplateTag";
 import ValidationError from "metabase-lib/lib/ValidationError";
+import { isFieldReference } from "metabase-lib/lib/references";
 import Dimension, { FieldDimension, TemplateTagDimension } from "../Dimension";
 import DimensionOptions from "../DimensionOptions";
 
@@ -561,9 +562,7 @@ export default class NativeQuery extends AtomicQuery {
     const templateTags = this.templateTags();
     return templateTags
       .filter(
-        tag =>
-          tag.type === "dimension" &&
-          FieldDimension.isFieldClause(tag.dimension),
+        tag => tag.type === "dimension" && isFieldReference(tag.dimension),
       )
       .map(tag => {
         const dimension = FieldDimension.parseMBQL(
diff --git a/frontend/src/metabase-lib/lib/queries/utils/dataset.js b/frontend/src/metabase-lib/lib/queries/utils/dataset.js
index 3cddda337b3..263663beabb 100644
--- a/frontend/src/metabase-lib/lib/queries/utils/dataset.js
+++ b/frontend/src/metabase-lib/lib/queries/utils/dataset.js
@@ -1,10 +1,7 @@
 import _ from "underscore";
 
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
-import Dimension, {
-  AggregationDimension,
-  FieldDimension,
-} from "metabase-lib/lib/Dimension";
+import Dimension, { FieldDimension } from "metabase-lib/lib/Dimension";
 
 export const datasetContainsNoResults = data =>
   data.rows == null || data.rows.length === 0;
@@ -37,36 +34,6 @@ export function fieldRefWithOption(fieldRef, key, value) {
   return dimension && dimension.withOption(key, value).mbql();
 }
 
-export const keyForColumn = column => {
-  let fieldRef = column.field_ref;
-  if (!fieldRef) {
-    console.error("column is missing field_ref", column);
-    fieldRef = new FieldDimension(column.name).mbql();
-  }
-
-  let dimension = Dimension.parseMBQL(fieldRef);
-  if (!dimension) {
-    console.warn("Unknown field_ref", fieldRef);
-    return JSON.stringify(fieldRef);
-  }
-
-  dimension = dimension.baseDimension();
-
-  // match bug where field w/ join alias returned field w/o join alias instead
-  if (dimension instanceof FieldDimension) {
-    dimension = dimension.withoutOptions("join-alias");
-  }
-
-  // match legacy behavior which didn't have "field-literal" or "aggregation" field refs
-  const isLegacyRef =
-    (dimension instanceof FieldDimension && dimension.isStringFieldName()) ||
-    dimension instanceof AggregationDimension;
-
-  return JSON.stringify(
-    isLegacyRef ? ["name", column.name] : ["ref", dimension.mbql()],
-  );
-};
-
 /**
  * finds the column object from the dataset results for the given `table.columns` column setting
  * @param  {Column[]} columns             Dataset results columns
diff --git a/frontend/src/metabase-lib/lib/queries/utils/get-column-key.ts b/frontend/src/metabase-lib/lib/queries/utils/get-column-key.ts
new file mode 100644
index 00000000000..23b0d25370d
--- /dev/null
+++ b/frontend/src/metabase-lib/lib/queries/utils/get-column-key.ts
@@ -0,0 +1,50 @@
+import { DatasetColumn } from "metabase-types/api";
+import {
+  createFieldReference,
+  getBaseDimensionReference,
+  getDimensionReferenceWithoutOptions,
+  getNormalizedDimensionReference,
+  hasStringFieldName,
+  isAggregationReference,
+  isExpressionReference,
+  isFieldReference,
+  isValidDimensionReference,
+} from "metabase-lib/lib/references";
+
+export const getColumnKey = (
+  column: Pick<DatasetColumn, "name" | "field_ref">,
+) => {
+  let fieldRef = column.field_ref;
+
+  if (!fieldRef) {
+    console.error("column is missing field_ref", column);
+    fieldRef = createFieldReference(column.name);
+  }
+
+  if (!isValidDimensionReference(fieldRef)) {
+    console.warn("Unknown field_ref", fieldRef);
+    return JSON.stringify(fieldRef);
+  }
+
+  fieldRef = getNormalizedDimensionReference(fieldRef);
+
+  if (
+    isFieldReference(fieldRef) ||
+    isExpressionReference(fieldRef) ||
+    isAggregationReference(fieldRef)
+  ) {
+    fieldRef = getBaseDimensionReference(fieldRef);
+  }
+
+  if (isFieldReference(fieldRef)) {
+    fieldRef = getDimensionReferenceWithoutOptions(fieldRef, ["join-alias"]);
+  }
+
+  const isLegacyRef =
+    (isFieldReference(fieldRef) && hasStringFieldName(fieldRef)) ||
+    isAggregationReference(fieldRef);
+
+  return JSON.stringify(
+    isLegacyRef ? ["name", column.name] : ["ref", fieldRef],
+  );
+};
diff --git a/frontend/src/metabase-lib/lib/queries/utils/get-column-key.unit.spec.js b/frontend/src/metabase-lib/lib/queries/utils/get-column-key.unit.spec.js
new file mode 100644
index 00000000000..94a60f8e5f3
--- /dev/null
+++ b/frontend/src/metabase-lib/lib/queries/utils/get-column-key.unit.spec.js
@@ -0,0 +1,91 @@
+import { getColumnKey } from "metabase-lib/lib/queries/utils/get-column-key";
+
+describe("getColumnKey", () => {
+  // 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 ...]] for field", () => {
+        expect(
+          getColumnKey({
+            name: "foo",
+            id: 1,
+            field_ref: fieldRefEnabled ? ["field", 1, null] : undefined,
+          }),
+        ).toEqual(JSON.stringify(["ref", ["field", 1, null]]));
+      });
+      it("should return [ref [field ...]] for foreign field", () => {
+        expect(
+          getColumnKey({
+            name: "foo",
+            id: 1,
+            fk_field_id: 2,
+            field_ref: fieldRefEnabled
+              ? ["field", 1, { "source-field": 2 }]
+              : undefined,
+          }),
+        ).toEqual(JSON.stringify(["ref", ["field", 1, { "source-field": 2 }]]));
+      });
+      it("should return [ref [expression ...]] for expression", () => {
+        expect(
+          getColumnKey({
+            name: "foo",
+            expression_name: "foo",
+            field_ref: fieldRefEnabled ? ["expression", "foo"] : undefined,
+          }),
+        ).toEqual(JSON.stringify(["ref", ["expression", "foo", null]]));
+      });
+      it("should return [name ...] for aggregation", () => {
+        const col = {
+          name: "foo",
+          source: "aggregation",
+          field_ref: fieldRefEnabled ? ["aggregation", 0] : undefined,
+        };
+        expect(getColumnKey(col, [col])).toEqual(
+          // NOTE: not ideal, matches existing behavior, but should be ["aggregation", 0]
+          JSON.stringify(["name", "foo"]),
+        );
+      });
+      it("should return [name ...] for aggregation", () => {
+        const col = {
+          name: "foo",
+          id: ["field", "foo", { "base-type": "type/Integer" }],
+          field_ref: fieldRefEnabled
+            ? ["field", "foo", { "base-type": "type/Integer" }]
+            : undefined,
+        };
+        expect(getColumnKey(col, [col])).toEqual(
+          // NOTE: not ideal, matches existing behavior, but should be ["field", "foo", {"base-type": "type/Integer"}]
+          JSON.stringify(["name", "foo"]),
+        );
+      });
+      it("should return [field ...] for native query column", () => {
+        expect(
+          getColumnKey({
+            name: "foo",
+            field_ref: fieldRefEnabled
+              ? ["field", "foo", { "base-type": "type/Integer" }]
+              : undefined,
+          }),
+        ).toEqual(
+          // NOTE: not ideal, matches existing behavior, but should be ["field", "foo", {"base-type": "type/Integer"}]
+          JSON.stringify(["name", "foo"]),
+        );
+      });
+    });
+  }
+
+  describe("with field_ref", () => {
+    it("should return [ref [field ...]] for joined field", () => {
+      const col = {
+        name: "foo",
+        id: 1,
+        field_ref: ["field", 1, { "join-alias": "x" }],
+      };
+      expect(getColumnKey(col)).toEqual(
+        // NOTE: not ideal, matches existing behavior, but should be ["field", 1, {"join-alias": "x"}]
+        JSON.stringify(["ref", ["field", 1, null]]),
+      );
+    });
+  });
+});
diff --git a/frontend/src/metabase-lib/lib/references.ts b/frontend/src/metabase-lib/lib/references.ts
new file mode 100644
index 00000000000..bdd7a4cc122
--- /dev/null
+++ b/frontend/src/metabase-lib/lib/references.ts
@@ -0,0 +1,124 @@
+import _ from "underscore";
+import {
+  AggregationReference,
+  ColumnName,
+  DimensionReference,
+  DimensionReferenceWithOptions,
+  ExpressionReference,
+  FieldId,
+  FieldReference,
+  ReferenceOptions,
+  TemplateTagReference,
+} from "metabase-types/api/query";
+
+export const isFieldReference = (mbql: any): mbql is FieldReference => {
+  return Array.isArray(mbql) && mbql.length === 3 && mbql[0] === "field";
+};
+
+export const isExpressionReference = (
+  mbql: any,
+): mbql is ExpressionReference => {
+  return Array.isArray(mbql) && mbql.length >= 2 && mbql[0] === "expression";
+};
+
+export const isAggregationReference = (
+  mbql: any,
+): mbql is AggregationReference => {
+  return Array.isArray(mbql) && mbql[0] === "aggregation";
+};
+
+export const isTemplateTagReference = (
+  mbql: any,
+): mbql is TemplateTagReference => {
+  return Array.isArray(mbql) && mbql[0] === "template-tag";
+};
+
+export const createFieldReference = (
+  columnNameOrFieldId: ColumnName | FieldId,
+): FieldReference => ["field", columnNameOrFieldId, null];
+
+export const isValidDimensionReference = (
+  mbql: any,
+): mbql is DimensionReference => {
+  return [
+    isFieldReference,
+    isExpressionReference,
+    isAggregationReference,
+    isTemplateTagReference,
+  ].some(predicate => predicate(mbql));
+};
+
+export const normalizeReferenceOptions = (
+  options?: ReferenceOptions | null,
+): ReferenceOptions | null => {
+  if (!options) {
+    return null;
+  }
+
+  // recursively normalize maps inside options.
+  options = _.mapObject(options, val =>
+    typeof val === "object" ? normalizeReferenceOptions(val) : val,
+  );
+  // remove null/undefined options from map.
+  options = _.omit(options, value => value == null);
+  return _.isEmpty(options) ? null : options;
+};
+
+export const getNormalizedDimensionReference = (
+  mbql: DimensionReference,
+): DimensionReference => {
+  if (
+    isFieldReference(mbql) ||
+    isExpressionReference(mbql) ||
+    isAggregationReference(mbql)
+  ) {
+    const normalizedReference = [...mbql] as DimensionReference;
+    const normalizedOptions = normalizeReferenceOptions(mbql[2]);
+    normalizedReference[2] = normalizedOptions;
+
+    return normalizedReference;
+  }
+
+  return mbql;
+};
+
+export const getDimensionReferenceWithoutOptions = (
+  mbql: DimensionReferenceWithOptions,
+  optionsKeysToOmit: string[],
+): DimensionReferenceWithOptions => {
+  const newReference = mbql.slice() as DimensionReferenceWithOptions;
+  const options = newReference[2];
+
+  if (!options) {
+    return newReference;
+  }
+
+  newReference[2] =
+    options == null ? null : _.omit(options, ...optionsKeysToOmit);
+
+  if (_.isEmpty(newReference[2])) {
+    newReference[2] = null;
+  }
+
+  return newReference;
+};
+
+export const BASE_DIMENSION_REFERENCE_OMIT_OPTIONS = [
+  "temporal-unit",
+  "binning",
+];
+
+export const getBaseDimensionReference = (
+  mbql: DimensionReferenceWithOptions,
+) =>
+  getDimensionReferenceWithoutOptions(
+    mbql,
+    BASE_DIMENSION_REFERENCE_OMIT_OPTIONS,
+  );
+
+/**
+ * Whether this Field clause has a string Field name (as opposed to an integer Field ID). This generally means the
+ * Field comes from a native query.
+ */
+export const hasStringFieldName = (mbql: FieldReference) =>
+  typeof mbql[1] === "string";
diff --git a/frontend/src/metabase-lib/lib/references.unit.spec.ts b/frontend/src/metabase-lib/lib/references.unit.spec.ts
new file mode 100644
index 00000000000..e70e5773be0
--- /dev/null
+++ b/frontend/src/metabase-lib/lib/references.unit.spec.ts
@@ -0,0 +1,131 @@
+import {
+  isFieldReference,
+  isExpressionReference,
+  isAggregationReference,
+  isTemplateTagReference,
+  normalizeReferenceOptions,
+} from "metabase-lib/lib/references";
+
+describe("reference predicates", () => {
+  describe("isFieldReference", () => {
+    it("returns true for valid field references", () => {
+      const validReferences = [
+        ["field", 123, null],
+        ["field", "column_name", null],
+        ["field", "column_name", {}],
+      ];
+
+      validReferences.forEach(ref => expect(isFieldReference(ref)).toBe(true));
+    });
+
+    it("returns false for invalid field references", () => {
+      const invalidReferences = [
+        null,
+        "field",
+        ["field", 123],
+        ["aggregation", 123, null],
+        ["field", "column_name", null, { "source-field": true }],
+      ];
+
+      invalidReferences.forEach(ref =>
+        expect(isFieldReference(ref)).toBe(false),
+      );
+    });
+  });
+
+  describe("isAggregationReference", () => {
+    it("returns true for valid aggregation references", () => {
+      const validReferences = [
+        ["aggregation", 123, null],
+        ["aggregation", "string", null],
+        ["aggregation", "column_name", { "base-type": "number" }],
+      ];
+
+      validReferences.forEach(ref =>
+        expect(isAggregationReference(ref)).toBe(true),
+      );
+    });
+
+    it("returns false for invalid aggregation references", () => {
+      const invalidReferences = [null, 123, "aggregation", ["field", 123]];
+
+      invalidReferences.forEach(ref =>
+        expect(isAggregationReference(ref)).toBe(false),
+      );
+    });
+  });
+
+  describe("isExpressionReference", () => {
+    it("returns true for valid expression references", () => {
+      const validReferences = [
+        ["expression", "string", null],
+        ["expression", "column_name", { "base-type": "number" }],
+      ];
+
+      validReferences.forEach(ref =>
+        expect(isExpressionReference(ref)).toBe(true),
+      );
+    });
+
+    it("returns false for invalid expression references", () => {
+      const invalidReferences = [
+        null,
+        123,
+        "expression",
+        ["expression"],
+        ["field", 123],
+      ];
+
+      invalidReferences.forEach(ref =>
+        expect(isExpressionReference(ref)).toBe(false),
+      );
+    });
+  });
+
+  describe("isTemplateTagReference", () => {
+    it("returns true for valid template tag references", () => {
+      const validReferences = [["template-tag", "tag name"]];
+
+      validReferences.forEach(ref =>
+        expect(isTemplateTagReference(ref)).toBe(true),
+      );
+    });
+
+    it("returns false for invalid template tag references", () => {
+      const invalidReferences = [null, "tag", ["templateTag", 123]];
+
+      invalidReferences.forEach(ref =>
+        expect(isTemplateTagReference(ref)).toBe(false),
+      );
+    });
+  });
+});
+
+describe("normalizeReferenceOptions", () => {
+  it("should remove empty options map", () => {
+    expect(normalizeReferenceOptions(null)).toEqual(null);
+    expect(normalizeReferenceOptions({})).toEqual(null);
+  });
+  it("should remove null/undefined keys", () => {
+    expect(
+      normalizeReferenceOptions({
+        "temporal-unit": undefined,
+        binning: { strategy: "default" },
+        "base-type": undefined,
+      }),
+    ).toEqual({
+      binning: { strategy: "default" },
+    });
+  });
+  it("should recursively normalize maps options", () => {
+    expect(
+      normalizeReferenceOptions({
+        binning: { strategy: "default", other: null } as any,
+      }),
+    ).toStrictEqual({
+      binning: { strategy: "default" },
+    });
+  });
+  // TODO -- it should also remove empty arrays, but we currently don't have any situations where there might be
+  // one.
+});
diff --git a/frontend/src/metabase-types/api/dataset.ts b/frontend/src/metabase-types/api/dataset.ts
index 7db0792e60b..3f2ab32a6d4 100644
--- a/frontend/src/metabase-types/api/dataset.ts
+++ b/frontend/src/metabase-types/api/dataset.ts
@@ -1,4 +1,7 @@
-import type { DatetimeUnit } from "metabase-types/api/query";
+import type {
+  DatetimeUnit,
+  DimensionReference,
+} from "metabase-types/api/query";
 import { DatabaseId } from "./database";
 import { DownloadPermission } from "./permissions";
 
@@ -6,11 +9,14 @@ export type RowValue = string | number | null | boolean;
 export type RowValues = RowValue[];
 
 export interface DatasetColumn {
+  id?: number;
   display_name: string;
   source: string;
   name: string;
   remapped_to_column?: DatasetColumn;
   unit?: DatetimeUnit;
+  field_ref?: DimensionReference;
+  expression_name?: any;
 }
 
 export interface DatasetData {
diff --git a/frontend/src/metabase-types/api/query.ts b/frontend/src/metabase-types/api/query.ts
index 45ec40e6ac0..0b3007fb365 100644
--- a/frontend/src/metabase-types/api/query.ts
+++ b/frontend/src/metabase-types/api/query.ts
@@ -40,3 +40,70 @@ export type DatetimeUnit =
   | "quarter"
   | "quarter-of-year"
   | "year";
+
+export interface ReferenceOptions {
+  binning?: BinningOptions;
+  "temporal-unit"?: DatetimeUnit;
+  "join-alias"?: string;
+  "base-type"?: string;
+}
+
+type BinningOptions =
+  | DefaultBinningOptions
+  | NumBinsBinningOptions
+  | BinWidthBinningOptions;
+
+interface DefaultBinningOptions {
+  strategy: "default";
+}
+
+interface NumBinsBinningOptions {
+  strategy: "num-bins";
+  "num-bins": number;
+}
+
+interface BinWidthBinningOptions {
+  strategy: "bin-width";
+  "bin-width": number;
+}
+
+export type ReferenceOptionsKeys =
+  | "source-field"
+  | "base-type"
+  | "join-alias"
+  | "temporal-unit"
+  | "binning";
+
+export type FieldId = number;
+export type ColumnName = string;
+export type FieldReference = [
+  "field",
+  FieldId | ColumnName,
+  ReferenceOptions | null,
+];
+
+export type ExpressionName = string;
+export type ExpressionReference = [
+  "expression",
+  ExpressionName,
+  ReferenceOptions | null,
+];
+
+export type AggregationIndex = number;
+export type AggregationReference = [
+  "aggregation",
+  AggregationIndex,
+  ReferenceOptions | null,
+];
+
+export type TagName = string;
+export type TemplateTagReference = ["template-tag", TagName];
+
+export type DimensionReferenceWithOptions =
+  | FieldReference
+  | ExpressionReference
+  | AggregationReference;
+
+export type DimensionReference =
+  | DimensionReferenceWithOptions
+  | TemplateTagReference;
diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebar.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebar.tsx
index a81834c7f36..1b091299b20 100644
--- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebar.tsx
+++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebar.tsx
@@ -16,12 +16,12 @@ import type {
   CardId,
   ClickBehavior,
   DatasetData,
+  DatasetColumn,
 } from "metabase-types/api";
-import type { Column } from "metabase-types/types/Dataset";
 import { isTableDisplay } from "metabase/lib/click-behavior";
 import { clickBehaviorIsValid } from "metabase-lib/lib/parameters/utils/click-behavior";
-import { keyForColumn } from "metabase-lib/lib/queries/utils/dataset";
 
+import { getColumnKey } from "metabase-lib/lib/queries/utils/get-column-key";
 import { getClickBehaviorForColumn } from "./utils";
 import ClickBehaviorSidebarContent from "./ClickBehaviorSidebarContent";
 import ClickBehaviorSidebarHeader from "./ClickBehaviorSidebarHeader";
@@ -67,7 +67,9 @@ function ClickBehaviorSidebar({
     boolean | null
   >(null);
 
-  const [selectedColumn, setSelectedColumn] = useState<Column | null>(null);
+  const [selectedColumn, setSelectedColumn] = useState<DatasetColumn | null>(
+    null,
+  );
 
   const [originalVizSettings, setOriginalVizSettings] = useState<
     VizSettings | undefined | null
@@ -105,7 +107,7 @@ function ClickBehaviorSidebar({
           click_behavior: nextClickBehavior,
         });
       } else {
-        onUpdateDashCardColumnSettings(id, keyForColumn(selectedColumn), {
+        onUpdateDashCardColumnSettings(id, getColumnKey(selectedColumn), {
           click_behavior: nextClickBehavior,
         });
       }
diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarContent.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarContent.tsx
index b1c835b72a0..1b195761137 100644
--- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarContent.tsx
+++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarContent.tsx
@@ -11,8 +11,8 @@ import type {
   CardId,
   ClickBehavior,
   DatasetData,
+  DatasetColumn,
 } from "metabase-types/api";
-import type { Column } from "metabase-types/types/Dataset";
 
 import { isTableDisplay } from "metabase/lib/click-behavior";
 import { getClickBehaviorForColumn } from "./utils";
@@ -29,7 +29,7 @@ interface Props {
   clickBehavior?: ClickBehavior;
   isTypeSelectorVisible: boolean | null;
   hasSelectedColumn: boolean;
-  onColumnSelected: (column: Column) => void;
+  onColumnSelected: (column: DatasetColumn) => void;
   onSettingsChange: (clickBehavior?: Partial<ClickBehavior>) => void;
   onTypeSelectorVisibilityChange: (isVisible: boolean) => void;
 }
@@ -62,7 +62,7 @@ function ClickBehaviorSidebar({
       <TableClickBehaviorView
         columns={columns}
         dashcard={dashcard}
-        getClickBehaviorForColumn={(column: Column) =>
+        getClickBehaviorForColumn={(column: DatasetColumn) =>
           getClickBehaviorForColumn(dashcard, column)
         }
         onColumnClick={onColumnSelected}
diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader/ClickBehaviorSidebarHeader.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader/ClickBehaviorSidebarHeader.tsx
index 7947f01d54c..30681a9150a 100644
--- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader/ClickBehaviorSidebarHeader.tsx
+++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/ClickBehaviorSidebarHeader/ClickBehaviorSidebarHeader.tsx
@@ -8,8 +8,7 @@ import {
   getActionButtonLabel,
 } from "metabase/writeback/utils";
 
-import type { DashboardOrderedCard } from "metabase-types/api";
-import type { Column } from "metabase-types/types/Dataset";
+import type { DashboardOrderedCard, DatasetColumn } from "metabase-types/api";
 
 import { isTableDisplay } from "metabase/lib/click-behavior";
 import { Heading, SidebarHeader } from "../ClickBehaviorSidebar.styled";
@@ -29,7 +28,7 @@ function DefaultHeader({ children }: { children: React.ReactNode }) {
 
 interface Props {
   dashcard: DashboardOrderedCard;
-  selectedColumn?: Column | null;
+  selectedColumn?: DatasetColumn | null;
   onUnsetColumn: () => void;
 }
 
diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/TableClickBehaviorView.tsx b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/TableClickBehaviorView.tsx
index 1a09ce97cd9..bdac0d2cd6a 100644
--- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/TableClickBehaviorView.tsx
+++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/TableClickBehaviorView/TableClickBehaviorView.tsx
@@ -6,8 +6,8 @@ import type {
   DashboardOrderedCard,
   ClickBehavior,
   ClickBehaviorType,
+  DatasetColumn,
 } from "metabase-types/api";
-import type { Column as IColumn } from "metabase-types/types/Dataset";
 
 import { hasActionsMenu } from "metabase/lib/click-behavior";
 import Column from "./Column";
@@ -33,10 +33,12 @@ function explainClickBehaviorType(
 }
 
 interface Props {
-  columns: IColumn[];
+  columns: DatasetColumn[];
   dashcard: DashboardOrderedCard;
-  getClickBehaviorForColumn: (column: IColumn) => ClickBehavior | undefined;
-  onColumnClick: (column: IColumn) => void;
+  getClickBehaviorForColumn: (
+    column: DatasetColumn,
+  ) => ClickBehavior | undefined;
+  onColumnClick: (column: DatasetColumn) => void;
 }
 
 function TableClickBehaviorView({
diff --git a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/utils.ts b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/utils.ts
index 9a35adba1bf..ce67d613588 100644
--- a/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/utils.ts
+++ b/frontend/src/metabase/dashboard/components/ClickBehaviorSidebar/utils.ts
@@ -4,10 +4,10 @@ import { getIn } from "icepick";
 import type {
   ClickBehaviorType,
   DashboardOrderedCard,
+  DatasetColumn,
 } from "metabase-types/api";
-import type { Column } from "metabase-types/types/Dataset";
 import { hasActionsMenu } from "metabase/lib/click-behavior";
-import { keyForColumn } from "metabase-lib/lib/queries/utils/dataset";
+import { getColumnKey } from "metabase-lib/lib/queries/utils/get-column-key";
 
 type ClickBehaviorOption = {
   value: ClickBehaviorType | "menu";
@@ -42,12 +42,12 @@ export function getClickBehaviorOptionName(
 }
 export function getClickBehaviorForColumn(
   dashcard: DashboardOrderedCard,
-  column: Column,
+  column: DatasetColumn,
 ) {
   return getIn(dashcard, [
     "visualization_settings",
     "column_settings",
-    keyForColumn(column),
+    getColumnKey(column),
     "click_behavior",
   ]);
 }
diff --git a/frontend/src/metabase/modes/components/drill/FormatAction.jsx b/frontend/src/metabase/modes/components/drill/FormatAction.jsx
index d59912ff870..34ae41233db 100644
--- a/frontend/src/metabase/modes/components/drill/FormatAction.jsx
+++ b/frontend/src/metabase/modes/components/drill/FormatAction.jsx
@@ -7,7 +7,7 @@ import { t } from "ttag";
 import { getSettingsWidgetsForSeries } from "metabase/visualizations/lib/settings/visualization";
 import ChartSettingsWidget from "metabase/visualizations/components/ChartSettingsWidget";
 import { updateSettings } from "metabase/visualizations/lib/settings";
-import { keyForColumn } from "metabase-lib/lib/queries/utils/dataset";
+import { getColumnKey } from "metabase-lib/lib/queries/utils/get-column-key";
 
 export default ({ question, clicked }) => {
   if (
@@ -56,7 +56,7 @@ export default ({ question, clicked }) => {
                 ...columnSettingsWidget,
                 props: {
                   ...columnSettingsWidget.props,
-                  initialKey: keyForColumn(column),
+                  initialKey: getColumnKey(column),
                 },
               }}
               hidden={false}
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/DefaultPicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/DefaultPicker.jsx
index c599259e549..5ab0da8a310 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/DefaultPicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/DefaultPicker.jsx
@@ -13,7 +13,7 @@ import {
   isFuzzyOperator,
 } from "metabase-lib/lib/operators/utils";
 import { isCurrency } from "metabase-lib/lib/types/utils/isa";
-import { keyForColumn } from "metabase-lib/lib/queries/utils/dataset";
+import { getColumnKey } from "metabase-lib/lib/queries/utils/get-column-key";
 import TextPicker from "./TextPicker";
 import SelectPicker from "./SelectPicker";
 import NumberPicker from "./NumberPicker";
@@ -66,7 +66,7 @@ export default function DefaultPicker({
 
   const visualizationSettings = filter?.query()?.question()?.settings();
 
-  const key = keyForColumn(dimension.column());
+  const key = getColumnKey(dimension.column());
   const columnSettings = visualizationSettings?.column_settings?.[key];
 
   const fieldMetadata = field?.metadata?.fields[field?.id];
diff --git a/frontend/src/metabase/static-viz/components/RowChart/utils/format.ts b/frontend/src/metabase/static-viz/components/RowChart/utils/format.ts
index 0c97a879f35..b7a5ec72f8b 100644
--- a/frontend/src/metabase/static-viz/components/RowChart/utils/format.ts
+++ b/frontend/src/metabase/static-viz/components/RowChart/utils/format.ts
@@ -3,6 +3,7 @@ import { ChartColumns } from "metabase/visualizations/lib/graph/columns";
 import { getStackOffset } from "metabase/visualizations/lib/settings/stacking";
 import { formatNumber, formatPercent } from "metabase/static-viz/lib/numbers";
 import { ChartTicksFormatters } from "metabase/visualizations/shared/types/format";
+import { getColumnKey } from "metabase-lib/lib/queries/utils/get-column-key";
 
 export const getXValueMetricColumn = (chartColumns: ChartColumns) => {
   // For multi-metrics charts we use the first metic column settings for formatting
@@ -21,7 +22,9 @@ export const getStaticFormatters = (
   };
 
   const metricColumnSettings =
-    settings.column_settings?.[getXValueMetricColumn(chartColumns).column.name];
+    settings.column_settings?.[
+      getColumnKey(getXValueMetricColumn(chartColumns).column)
+    ];
 
   const xTickFormatter = (value: any) =>
     formatNumber(value, metricColumnSettings);
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx
index 2729e06ad6a..c6f2f984e79 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx
@@ -2,7 +2,7 @@
 import React from "react";
 import { t } from "ttag";
 import _ from "underscore";
-import { keyForColumn } from "metabase-lib/lib/queries/utils/dataset";
+import { getColumnKey } from "metabase-lib/lib/queries/utils/get-column-key";
 import ChartSettingSelect from "./ChartSettingSelect";
 import {
   SettingsIcon,
@@ -25,7 +25,7 @@ const ChartSettingFieldPicker = ({
   if (value && showColumnSetting && columns) {
     const column = _.findWhere(columns, { name: value });
     if (column && columnHasSettings(column)) {
-      columnKey = keyForColumn(column);
+      columnKey = getColumnKey(column);
     }
   }
   return (
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPartition.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPartition.jsx
index 8cd5ee55aa1..b33fa9d2b85 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPartition.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPartition.jsx
@@ -8,7 +8,7 @@ import { splice } from "icepick";
 
 import Label from "metabase/components/type/Label";
 
-import { keyForColumn } from "metabase-lib/lib/queries/utils/dataset";
+import { getColumnKey } from "metabase-lib/lib/queries/utils/get-column-key";
 import {
   DroppableContainer,
   FieldPartitionColumn,
@@ -40,7 +40,7 @@ class ChartSettingFieldsPartition extends React.Component {
         {
           id: "column_settings",
           props: {
-            initialKey: keyForColumn(column),
+            initialKey: getColumnKey(column),
           },
         },
         targetElement,
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedColumns.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedColumns.jsx
index 6526cde281f..f3750bfe9b8 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedColumns.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedColumns.jsx
@@ -4,10 +4,8 @@ import { t } from "ttag";
 import _ from "underscore";
 
 import { getFriendlyName } from "metabase/visualizations/lib/utils";
-import {
-  keyForColumn,
-  findColumnForColumnSetting,
-} from "metabase-lib/lib/queries/utils/dataset";
+import { getColumnKey } from "metabase-lib/lib/queries/utils/get-column-key";
+import { findColumnForColumnSetting } from "metabase-lib/lib/queries/utils/dataset";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 
 import ColumnItem from "./ColumnItem";
@@ -45,7 +43,7 @@ export default class ChartSettingOrderedColumns extends Component {
         {
           id: "column_settings",
           props: {
-            initialKey: keyForColumn(column),
+            initialKey: getColumnKey(column),
           },
         },
         targetElement,
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingsListColumns/ChartSettingsListColumns.tsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingsListColumns/ChartSettingsListColumns.tsx
index 18a64ff2a2b..3c807d92c8d 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingsListColumns/ChartSettingsListColumns.tsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingsListColumns/ChartSettingsListColumns.tsx
@@ -4,10 +4,10 @@ import _ from "underscore";
 
 import Button from "metabase/core/components/Button";
 
-import { Column } from "metabase-types/types/Dataset";
 import { FieldId } from "metabase-types/types/Field";
 import { ConcreteField } from "metabase-types/types/Query";
-import { keyForColumn } from "metabase-lib/lib/queries/utils/dataset";
+import { DatasetColumn } from "metabase-types/api";
+import { getColumnKey } from "metabase-lib/lib/queries/utils/get-column-key";
 import Question from "metabase-lib/lib/Question";
 
 import {
@@ -25,7 +25,7 @@ type SettingValue = {
 
 interface Props {
   value: SettingValue;
-  columns: Column[];
+  columns: DatasetColumn[];
   question: Question;
   onChange: (value: SettingValue) => void;
   onShowWidget: (config: unknown, targetElement: HTMLElement | null) => void;
@@ -78,7 +78,7 @@ function ChartSettingsListColumns({
           {
             id: "column_settings",
             props: {
-              initialKey: keyForColumn(column),
+              initialKey: getColumnKey(column),
             },
           },
           targetElement,
diff --git a/frontend/src/metabase/visualizations/lib/settings/column.js b/frontend/src/metabase/visualizations/lib/settings/column.js
index d863644152c..52af23bb00e 100644
--- a/frontend/src/metabase/visualizations/lib/settings/column.js
+++ b/frontend/src/metabase/visualizations/lib/settings/column.js
@@ -33,7 +33,7 @@ export function columnSettings({
     section: t`Formatting`,
     objectName: "column",
     getObjects: getColumns,
-    getObjectKey: keyForColumn,
+    getObjectKey: getColumnKey,
     getSettingDefintionsForObject: getSettingDefintionsForColumn,
     component: ChartNestedSettingColumns,
     getInheritedSettingsForObject: getInhertiedSettingsForColumn,
@@ -50,7 +50,7 @@ import {
   isCurrency,
   isDateWithoutTime,
 } from "metabase-lib/lib/types/utils/isa";
-import { keyForColumn } from "metabase-lib/lib/queries/utils/dataset";
+import { getColumnKey } from "metabase-lib/lib/queries/utils/get-column-key";
 import { nestedSettings } from "./nested";
 
 export function getGlobalSettingsForColumn(column) {
diff --git a/frontend/test/metabase-lib/lib/Dimension.unit.spec.js b/frontend/test/metabase-lib/lib/Dimension.unit.spec.js
index 88272fd527a..ad61e28f810 100644
--- a/frontend/test/metabase-lib/lib/Dimension.unit.spec.js
+++ b/frontend/test/metabase-lib/lib/Dimension.unit.spec.js
@@ -213,31 +213,6 @@ describe("Dimension", () => {
       metadata,
     );
 
-    describe("STATIC METHODS", () => {
-      describe("normalizeOptions()", () => {
-        it("should remove empty options map", () => {
-          expect(FieldDimension.normalizeOptions(null)).toEqual(null);
-          expect(FieldDimension.normalizeOptions({})).toEqual(null);
-        });
-        it("should remove null/undefined keys", () => {
-          expect(
-            FieldDimension.normalizeOptions({
-              x: false,
-              y: null,
-              z: undefined,
-            }),
-          ).toEqual({ x: false });
-        });
-        it("should recursively normalize maps options", () => {
-          expect(
-            FieldDimension.normalizeOptions({ binning: { x: null } }),
-          ).toBe(null);
-        });
-        // TODO -- it should also remove empty arrays, but we currently don't have any situations where there might be
-        // one.
-      });
-    });
-
     describe("INSTANCE METHODS", () => {
       describe("mbql()", () => {
         it(
@@ -940,24 +915,6 @@ describe("Dimension", () => {
             ).toBeNull();
           });
         });
-
-        describe("isTemplateTagClause", () => {
-          it("returns false for a field clause", () => {
-            expect(
-              TemplateTagDimension.isTemplateTagClause(["field", 123, null]),
-            ).toBe(false);
-          });
-
-          it("returns false for a non-array clause", () => {
-            expect(TemplateTagDimension.isTemplateTagClause("foo")).toBe(false);
-          });
-
-          it("returns true for a template tag clause", () => {
-            expect(
-              TemplateTagDimension.isTemplateTagClause(templateTagClause),
-            ).toBe(true);
-          });
-        });
       });
 
       describe("INSTANCE METHODS", () => {
@@ -1095,24 +1052,6 @@ describe("Dimension", () => {
             ).toBeNull();
           });
         });
-
-        describe("isTemplateTagClause", () => {
-          it("returns false for a field clause", () => {
-            expect(
-              TemplateTagDimension.isTemplateTagClause(["field", 123, null]),
-            ).toBe(false);
-          });
-
-          it("returns false for a non-array clause", () => {
-            expect(TemplateTagDimension.isTemplateTagClause("foo")).toBe(false);
-          });
-
-          it("returns true for a template tag clause", () => {
-            expect(
-              TemplateTagDimension.isTemplateTagClause(templateTagClause),
-            ).toBe(true);
-          });
-        });
       });
 
       describe("INSTANCE METHODS", () => {
diff --git a/frontend/test/metabase/lib/dataset.unit.spec.js b/frontend/test/metabase/lib/dataset.unit.spec.js
index 5573c0646e4..06235baf8e3 100644
--- a/frontend/test/metabase/lib/dataset.unit.spec.js
+++ b/frontend/test/metabase/lib/dataset.unit.spec.js
@@ -3,7 +3,6 @@ import {
   fieldRefForColumn,
   syncTableColumnsToQuery,
   findColumnForColumnSetting,
-  keyForColumn,
 } from "metabase-lib/lib/queries/utils/dataset";
 
 describe("metabase/util/dataset", () => {
@@ -177,96 +176,4 @@ 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 ...]] for field", () => {
-          expect(
-            keyForColumn({
-              name: "foo",
-              id: 1,
-              field_ref: fieldRefEnabled ? ["field", 1, null] : undefined,
-            }),
-          ).toEqual(JSON.stringify(["ref", ["field", 1, null]]));
-        });
-        it("should return [ref [field ...]] for foreign field", () => {
-          expect(
-            keyForColumn({
-              name: "foo",
-              id: 1,
-              fk_field_id: 2,
-              field_ref: fieldRefEnabled
-                ? ["field", 1, { "source-field": 2 }]
-                : undefined,
-            }),
-          ).toEqual(
-            JSON.stringify(["ref", ["field", 1, { "source-field": 2 }]]),
-          );
-        });
-        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", null]]));
-        });
-        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 aggregation", () => {
-          const col = {
-            name: "foo",
-            id: ["field", "foo", { "base-type": "type/Integer" }],
-            field_ref: fieldRefEnabled
-              ? ["field", "foo", { "base-type": "type/Integer" }]
-              : undefined,
-          };
-          expect(keyForColumn(col, [col])).toEqual(
-            // NOTE: not ideal, matches existing behavior, but should be ["field", "foo", {"base-type": "type/Integer"}]
-            JSON.stringify(["name", "foo"]),
-          );
-        });
-        it("should return [field ...] for native query column", () => {
-          expect(
-            keyForColumn({
-              name: "foo",
-              field_ref: fieldRefEnabled
-                ? ["field", "foo", { "base-type": "type/Integer" }]
-                : undefined,
-            }),
-          ).toEqual(
-            // NOTE: not ideal, matches existing behavior, but should be ["field", "foo", {"base-type": "type/Integer"}]
-            JSON.stringify(["name", "foo"]),
-          );
-        });
-      });
-    }
-
-    describe("with field_ref", () => {
-      it("should return [ref [field ...]] for joined field", () => {
-        const col = {
-          name: "foo",
-          id: 1,
-          field_ref: ["field", 1, { "join-alias": "x" }],
-        };
-        expect(keyForColumn(col)).toEqual(
-          // NOTE: not ideal, matches existing behavior, but should be ["field", 1, {"join-alias": "x"}]
-          JSON.stringify(["ref", ["field", 1, null]]),
-        );
-      });
-    });
-  });
 });
-- 
GitLab