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