diff --git a/frontend/src/metabase-lib/metadata/Database.ts b/frontend/src/metabase-lib/metadata/Database.ts index 773a3aba76589db0be374887be3485a7b9fc52db..ee695a95dad8bceb9e9822c726b1d75bbe77818d 100644 --- a/frontend/src/metabase-lib/metadata/Database.ts +++ b/frontend/src/metabase-lib/metadata/Database.ts @@ -64,7 +64,7 @@ class Database { return this.tablesLookup(); } - hasFeature(feature: string | undefined) { + hasFeature(feature: string | null | undefined) { if (!feature) { return true; } diff --git a/frontend/src/metabase-lib/metadata/Database.unit.spec.ts b/frontend/src/metabase-lib/metadata/Database.unit.spec.ts index 7157cf0c04899f89c2aa5f414db95e1b55cba2f6..074d72afbf60573f4494e009637c255654bf7a1e 100644 --- a/frontend/src/metabase-lib/metadata/Database.unit.spec.ts +++ b/frontend/src/metabase-lib/metadata/Database.unit.spec.ts @@ -1,272 +1,260 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck -import Question from "../Question"; -import Database from "./Database"; -import Schema from "./Schema"; -import Metadata from "./Metadata"; -import Table from "./Table"; +import { Database } from "metabase-types/api"; +import { createMockMetadata } from "__support__/metadata"; +import { createMockDatabase, createMockTable } from "metabase-types/api/mocks"; +import NativeQuery from "metabase-lib/queries/NativeQuery"; +import StructuredQuery from "metabase-lib/queries/StructuredQuery"; + +interface SetupOpts { + database?: Database; + otherDatabases?: Database[]; +} + +const setup = ({ + database = createMockDatabase(), + otherDatabases = [], +}: SetupOpts = {}) => { + const metadata = createMockMetadata({ + databases: [database, ...otherDatabases], + }); + + const instance = metadata.database(database.id); + if (!instance) { + throw new TypeError(); + } + + return instance; +}; describe("Database", () => { describe("instantiation", () => { - it("should create an instance of Schema", () => { - expect(new Database()).toBeInstanceOf(Database); + it("should create an instance of Database", () => { + const database = setup({ + database: createMockDatabase({}), + }); + expect(database).toBeDefined(); }); }); + describe("displayName", () => { it("should return the name prop", () => { - expect( - new Database({ + const database = setup({ + database: createMockDatabase({ name: "foo", - }).displayName(), - ).toBe("foo"); + }), + }); + + expect(database.displayName()).toBe("foo"); }); }); + describe("schema", () => { - let schema; - let database; - beforeEach(() => { - schema = new Schema({ - id: "123:foo", - }); - const metadata = new Metadata({ - schemas: { - "123:foo": schema, - }, - }); - database = new Database({ - id: 123, - metadata, - }); - }); it("should return the schema with the given name", () => { - expect(database.schema("foo")).toBe(schema); - }); - it("should return null when the given schema name doesn not match a schema", () => { + const database = setup({ + database: createMockDatabase({ + tables: [ + createMockTable({ + schema: "public", + }), + ], + }), + }); + + expect(database.schema("public")).toBeDefined(); expect(database.schema("bar")).toBe(null); }); }); + describe("schemaNames", () => { it("should return a list of schemaNames", () => { - const database = new Database({ - id: 123, - schemas: [ - new Schema({ - id: "123:foo", - name: "foo", - }), - new Schema({ - id: "123:bar", - name: "bar", - }), - ], + const database = setup({ + database: createMockDatabase({ + tables: [ + createMockTable({ + id: 1, + schema: "foo", + }), + createMockTable({ + id: 2, + schema: "bar", + }), + ], + }), }); + expect(database.schemaNames()).toEqual(["bar", "foo"]); }); }); + describe("tablesLookup", () => { it("should return a map of tables keyed by id", () => { - const table1 = new Table({ - id: 1, - }); - const table2 = new Table({ - id: 2, - }); - expect( - new Database({ - tables: [], - }).tablesLookup(), - ).toEqual({}); - expect( - new Database({ - tables: [table1, table2], - }).tablesLookup(), - ).toEqual({ - 1: table1, - 2: table2, + const database = setup({ + database: createMockDatabase({ + tables: [ + createMockTable({ + id: 1, + }), + createMockTable({ + id: 2, + }), + ], + }), }); + + const lookup = database.tablesLookup(); + expect(lookup[1]).toBeDefined(); + expect(lookup[2]).toBeDefined(); + expect(lookup[1]).toBe(database.metadata?.table(1)); + expect(lookup[2]).toBe(database.metadata?.table(2)); }); }); + describe("hasFeature", () => { it("returns true when given a falsy `feature`", () => { - expect(new Database({}).hasFeature(null)).toBe(true); - expect(new Database({}).hasFeature("")).toBe(true); + const database = setup({ + database: createMockDatabase(), + }); + + expect(database.hasFeature(null)).toBe(true); + expect(database.hasFeature("")).toBe(true); }); + it("should return true when given `feature` is found within the `features` on the instance", () => { - expect( - new Database({ - features: ["foo"], - }).hasFeature("foo"), - ).toBe(true); + const database = setup({ + database: createMockDatabase({ + features: ["inner-join"], + }), + }); + + expect(database.hasFeature("inner-join")).toBe(true); }); + it("should return false when given `feature` is not found within the `features` on the instance", () => { - expect( - new Database({ - features: ["foo"], - }).hasFeature("bar"), - ).toBe(false); - }); - it("should return false for 'join' even when it exists in `features`", () => { - expect( - new Database({ - features: ["join"], - }).hasFeature("join"), - ).toBe(false); - }); - it("should return true for 'join' for a set of other values", () => { - ["left-join", "right-join", "inner-join", "full-join"].forEach( - feature => { - expect( - new Database({ - features: [feature], - }).hasFeature("join"), - ).toBe(true); - }, - ); + const database = setup({ + database: createMockDatabase({ + features: ["inner-join"], + }), + }); + + expect(database.hasFeature("persist-models")).toBe(false); }); + + it.each(["left-join", "right-join", "inner-join", "full-join"] as const)( + "should return true for 'join' for %s", + feature => { + const database = setup({ + database: createMockDatabase({ + features: [feature], + }), + }); + + expect(database.hasFeature("join")).toBe(true); + }, + ); }); + describe("supportsPivots", () => { it("returns true when `expressions` and `left-join` exist in `features`", () => { - expect( - new Database({ - features: ["foo", "left-join"], - }).supportsPivots(), - ).toBe(false); - expect( - new Database({ - features: ["expressions", "right-join"], - }).supportsPivots(), - ).toBe(false); - expect( - new Database({ + const database = setup({ + database: createMockDatabase({ features: ["expressions", "left-join"], - }).supportsPivots(), - ).toBe(true); + }), + }); + + expect(database.supportsPivots()).toBe(true); + }); + + it("returns false when `expressions` and `left-join` not exist in `features`", () => { + const database = setup({ + database: createMockDatabase({ + features: ["schemas", "persist-models"], + }), + }); + + expect(database.supportsPivots()).toBe(false); }); }); + describe("question", () => { it("should create a question using the `metadata` found on the Database instance", () => { - const metadata = new Metadata(); - const database = new Database({ - metadata, - }); + const database = setup(); const question = database.question(); - expect(question.metadata()).toBe(metadata); + + expect(question.query()).toBeInstanceOf(StructuredQuery); + expect(question.metadata()).toEqual(database.metadata); }); + it("should create a question using the given Database instance's id in the question's query", () => { - const database = new Database({ - id: 123, - }); - expect(database.question().datasetQuery()).toEqual({ - database: 123, - query: { - "source-table": undefined, - }, - type: "query", + const table = createMockTable(); + const database = setup({ + database: createMockDatabase({ tables: [table] }), }); - expect( - database - .question({ - foo: "bar", - }) - .datasetQuery(), - ).toEqual({ - database: 123, - query: { - foo: "bar", - }, - type: "query", + const question = database.question({ + "source-table": table.id, }); + + expect(question.databaseId()).toBe(database.id); + expect(question.tableId()).toBe(table.id); }); }); + describe("nativeQuestion", () => { it("should create a native question using the `metadata` found on the Database instance", () => { - const metadata = new Metadata(); - const database = new Database({ - metadata, - }); + const database = setup(); const question = database.nativeQuestion(); - expect(question.metadata()).toBe(metadata); + + expect(question.query()).toBeInstanceOf(NativeQuery); + expect(question.metadata()).toBe(database.metadata); }); + it("should create a native question using the given Database instance's id in the question's query", () => { - const database = new Database({ - id: 123, - }); - expect(database.nativeQuestion().datasetQuery()).toEqual({ - database: 123, - native: { - query: "", - "template-tags": {}, - }, - type: "native", - }); - expect( - database - .nativeQuestion({ - foo: "bar", - }) - .datasetQuery(), - ).toEqual({ - database: 123, - native: { - query: "", - "template-tags": {}, - foo: "bar", - }, - type: "native", - }); + const database = setup(); + const question = database.nativeQuestion({ query: "SELECT 1" }); + + const query = question.query() as NativeQuery; + expect(query.queryText()).toBe("SELECT 1"); }); }); + describe("newQuestion", () => { it("should return new question with defaulted query and display", () => { - const database = new Database({ - id: 123, - }); - Question.prototype.setDefaultQuery = jest.fn(function () { - return this; - }); - Question.prototype.setDefaultDisplay = jest.fn(function () { - return this; - }); + const database = setup(); const question = database.newQuestion(); - expect(question).toBeInstanceOf(Question); - expect(Question.prototype.setDefaultDisplay).toHaveBeenCalled(); - expect(Question.prototype.setDefaultQuery).toHaveBeenCalled(); + + expect(question.display()).toBe("table"); }); }); + describe("savedQuestionsDatabase", () => { it("should return the 'fake' saved questions database", () => { - const database1 = new Database({ - id: 1, - }); - const database2 = new Database({ - id: 2, - is_saved_questions: true, - }); - const metadata = new Metadata({ - databases: { - 1: database1, - 2: database2, - }, + const database = setup({ + database: createMockDatabase({ id: 1 }), + otherDatabases: [ + createMockDatabase({ id: 2, is_saved_questions: true }), + ], }); - database1.metadata = metadata; - expect(database1.savedQuestionsDatabase()).toBe(database2); + + const savedQuestionsDatabase = database.savedQuestionsDatabase(); + expect(savedQuestionsDatabase).toBeDefined(); + expect(savedQuestionsDatabase).toBe(database.metadata?.database(2)); }); }); describe("canWrite", () => { it("should be true for a db with write permissions", () => { - const database = new Database({ - id: 1, - native_permissions: "write", + const database = setup({ + database: createMockDatabase({ + native_permissions: "write", + }), }); expect(database.canWrite()).toBe(true); }); it("should be false for a db without write permissions", () => { - const database = new Database({ - id: 1, - native_permissions: "none", + const database = setup({ + database: createMockDatabase({ + native_permissions: "none", + }), }); expect(database.canWrite()).toBe(false); diff --git a/frontend/src/metabase-lib/metadata/Field.ts b/frontend/src/metabase-lib/metadata/Field.ts index cf1ccef0679c51379fa79b3657433bd984d2f749..b484082d66ea83efd373d75f6b0bcbeda57bca50 100644 --- a/frontend/src/metabase-lib/metadata/Field.ts +++ b/frontend/src/metabase-lib/metadata/Field.ts @@ -69,7 +69,7 @@ const LONG_TEXT_MIN = 80; */ class FieldInner extends Base { - id: number | FieldReference; + id: FieldId | FieldReference; name: string; display_name: string; description: string | null; @@ -140,7 +140,7 @@ class FieldInner extends Base { displayName({ includeSchema = false, - includeTable, + includeTable = false, includePath = true, } = {}) { let displayName = ""; diff --git a/frontend/src/metabase-lib/metadata/Field.unit.spec.ts b/frontend/src/metabase-lib/metadata/Field.unit.spec.ts index 0702654f5ca8459221c63f127f494d8e7c17d836..571b2103f3231af2445cca820df52c5c19d87085 100644 --- a/frontend/src/metabase-lib/metadata/Field.unit.spec.ts +++ b/frontend/src/metabase-lib/metadata/Field.unit.spec.ts @@ -1,142 +1,180 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck -import Dimension from "../Dimension"; -import Field from "./Field"; -import Table from "./Table"; -import Schema from "./Schema"; -import Metadata from "./Metadata"; -import Base from "./Base"; -import { createMockConcreteField } from "./mocks"; +import { Database, Field, Table } from "metabase-types/api"; +import { + createMockDateTimeFieldFingerprint, + createMockField, + createMockFieldDimension, + createMockTable, +} from "metabase-types/api/mocks"; +import { createMockMetadata } from "__support__/metadata"; +import { TYPE } from "metabase-lib/types/constants"; + +const FIELD_ID = 1; + +interface SetupOpts { + databases?: Database[]; + tables?: Table[]; + fields?: Field[]; +} + +const setup = ({ databases = [], tables = [], fields = [] }: SetupOpts) => { + const metadata = createMockMetadata({ + databases, + tables, + fields, + }); + + const instance = metadata.field(FIELD_ID); + if (!instance) { + throw TypeError(); + } + + return instance; +}; describe("Field", () => { describe("instantiation", () => { it("should create an instance of Schema", () => { - expect(new Field()).toBeInstanceOf(Field); - }); + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + }), + ], + }); - it("should add `object` props to the instance (because it extends Base)", () => { - expect(new Field()).toBeInstanceOf(Base); - expect( - new Field({ - foo: "bar", - }), - ).toHaveProperty("foo", "bar"); + expect(field).toBeDefined(); }); }); describe("parent", () => { it("should return null when `metadata` does not exist on instance", () => { - expect(new Field().parent()).toBeNull(); + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + }), + ], + }); + + expect(field.parent()).toBeNull(); }); it("should return the field that matches the instance's `parent_id` when `metadata` exists on the instance", () => { - const parentField = new Field({ - id: 1, - }); - const metadata = new Metadata({ - fields: { - 1: parentField, - }, - }); - const field = new Field({ - parent_id: 1, - id: 2, - metadata, + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + parent_id: 2, + }), + createMockField({ + id: 2, + }), + ], }); - expect(field.parent()).toBe(parentField); + + expect(field.parent()).toBeDefined(); + expect(field.parent()).toBe(field.metadata?.field(2)); }); }); describe("path", () => { it("should return list of fields starting with instance, ending with root parent", () => { - const rootField = new Field({ - id: 1, - }); - const parentField = new Field({ - id: 2, - parent_id: 1, - }); - const metadata = new Metadata({ - fields: { - 1: rootField, - 2: parentField, - }, - }); - parentField.metadata = metadata; - rootField.metadata = metadata; - const field = new Field({ - parent_id: 2, - id: 3, - metadata, - }); - expect(field.path()).toEqual([rootField, parentField, field]); + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + parent_id: 2, + }), + createMockField({ + id: 2, + parent_id: 3, + }), + createMockField({ + id: 3, + }), + ], + }); + + const metadata = field.metadata; + expect(field.path()).toEqual([ + metadata?.field(3), + metadata?.field(2), + metadata?.field(1), + ]); }); }); describe("displayName", () => { - it("should return a field's display name", () => { - expect( - new Field({ - name: "foo", - }).displayName(), - ).toBe("foo"); + it("should return a field's name", () => { + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + name: "foo", + display_name: "", + }), + ], + }); + + expect(field.displayName()).toBe("foo"); }); it("should prioritize the `display_name` field over `name`", () => { - expect( - new Field({ - display_name: "bar", - name: "foo", - }).displayName(), - ).toBe("bar"); + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + name: "foo", + display_name: "bar", + }), + ], + }); + + expect(field.displayName()).toBe("bar"); }); it("should prioritize the name in the field's `dimensions` property if it has one", () => { - const field = new Field({ - dimensions: [ - { - name: "dimensions", - }, + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + display_name: "display", + dimensions: [ + createMockFieldDimension({ + name: "dimensions", + }), + ], + }), ], - display_name: "display", }); + expect(field.displayName()).toBe("dimensions"); }); describe("includePath flag", () => { - let field; - beforeEach(() => { - const rootField = new Field({ - id: 1, - name: "rootField", - }); - const parentField = new Field({ - id: 2, - parent_id: 1, - name: "parentField", - }); - const metadata = new Metadata({ - fields: { - 1: rootField, - 2: parentField, - }, - }); - parentField.metadata = metadata; - rootField.metadata = metadata; - field = new Field({ - parent_id: 2, - id: 3, - metadata, - name: "field", - }); + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + parent_id: 2, + display_name: "field", + }), + createMockField({ + id: 2, + parent_id: 3, + display_name: "parentField", + }), + createMockField({ + id: 3, + display_name: "rootField", + }), + ], }); it("should add parent field display names to the field's display name when enabled", () => { - expect( - field.displayName({ - includePath: true, - }), - ).toBe("rootField: parentField: field"); + expect(field.displayName({ includePath: true })).toBe( + "rootField: parentField: field", + ); }); it("should be enabled by default", () => { @@ -148,53 +186,54 @@ describe("Field", () => { }); it("should exclude parent field display names when disabled", () => { - expect( - field.displayName({ - includePath: false, - }), - ).toBe("field"); + expect(field.displayName({ includePath: false })).toBe("field"); }); }); describe("includeTable flag", () => { - let field; - beforeEach(() => { - field = new Field({ - id: 1, - name: "field", + it("should do nothing when there is no table on the field instance", () => { + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + display_name: "field", + }), + ], }); - }); - it("should do nothing when there is no table on the field instance", () => { - expect( - field.displayName({ - includeTable: true, - }), - ).toBe("field"); + expect(field.displayName({ includeTable: true })).toBe("field"); }); it("should add the table name to the start of the field name", () => { - field.table = new Table({ - display_name: "table", + const field = setup({ + tables: [ + createMockTable({ + display_name: "table", + fields: [ + createMockField({ + id: FIELD_ID, + display_name: "field", + }), + ], + }), + ], }); - expect( - field.displayName({ - includeTable: true, - }), - ).toBe("table → field"); + + expect(field.displayName({ includeTable: true })).toBe("table → field"); }); }); describe("includeSchema flag", () => { - let field; - beforeEach(() => { - field = new Field({ - id: 1, - name: "field", + it("won't do anything if enabled and includeTable is not enabled", () => { + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + display_name: "field", + }), + ], }); - }); - it("won't do anything if enabled and includeTable is not enabled", () => { expect( field.displayName({ includeSchema: true, @@ -203,12 +242,21 @@ describe("Field", () => { }); it("should add a combined schema + table display name to the start of the field name", () => { - field.table = new Table({ - display_name: "table", - schema: new Schema({ - name: "schema", - }), + const field = setup({ + tables: [ + createMockTable({ + display_name: "table", + schema: "schema", + fields: [ + createMockField({ + id: FIELD_ID, + display_name: "field", + }), + ], + }), + ], }); + expect( field.displayName({ includeTable: true, @@ -221,43 +269,46 @@ describe("Field", () => { describe("targetObjectName", () => { it("should return the display name of the field stripped of an appended id", () => { - const field = new Field({ - name: "field id", + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + display_name: "field id", + }), + ], }); + expect(field.targetObjectName()).toBe("field"); }); }); describe("dimension", () => { - it("should return the field's dimension when the id is an mbql field", () => { - const field = new Field({ - id: ["field", 123, null], - }); - const dimension = field.dimension(); - expect(dimension).toBeInstanceOf(Dimension); - expect(dimension.fieldIdOrName()).toBe(123); - }); - it("should return the field's dimension when the id is not an mbql field", () => { - const field = new Field({ - id: 123, + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + display_name: "field id", + }), + ], }); + const dimension = field.dimension(); - expect(dimension).toBeInstanceOf(Dimension); - expect(dimension.fieldIdOrName()).toBe(123); + expect(dimension.fieldIdOrName()).toBe(FIELD_ID); }); }); describe("getDefaultDateTimeUnit", () => { describe("when the field is of type `type/DateTime`", () => { it("should return 'day'", () => { - const field = new Field({ - fingerprint: { - type: { - "type/Number": {}, - }, - }, + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + }), + ], }); + expect(field.getDefaultDateTimeUnit()).toBe("day"); }); }); @@ -265,207 +316,246 @@ describe("Field", () => { describe("when field is of type `type/DateTime`", () => { it("should return a time unit depending on the number of days in the 'fingerprint'", () => { - const field = new Field({ - fingerprint: { - type: { - "type/DateTime": { - earliest: "2019-03-01T00:00:00Z", - latest: "2021-01-01T00:00:00Z", + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + fingerprint: { + type: { + "type/DateTime": createMockDateTimeFieldFingerprint({ + earliest: "2019-03-01T00:00:00Z", + latest: "2021-01-01T00:00:00Z", + }), + }, }, - }, - }, + }), + ], }); + expect(field.getDefaultDateTimeUnit()).toBe("month"); }); }); describe("remappedField", () => { it("should return the 'human readable' field tied to the field's dimension", () => { - const field1 = new Field({ - id: 1, - }); - const field2 = new Field({ - id: 2, - }); - const metadata = new Metadata({ - fields: { - 1: field1, - 2: field2, - }, - }); - const field = new Field({ - id: 3, - dimensions: [ - { - human_readable_field_id: 1, - }, + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + dimensions: [ + createMockFieldDimension({ + human_readable_field_id: 2, + }), + ], + }), + createMockField({ + id: 2, + }), ], }); - field.metadata = metadata; - expect(field.remappedField()).toBe(field1); + + expect(field.remappedField()).toBeDefined(); + expect(field.remappedField()).toBe(field.metadata?.field(2)); }); it("should return the field's name_field", () => { - const nameField = new Field(); - const field = new Field({ - id: 3, - name_field: nameField, + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + name_field: createMockField({ + id: 2, + }), + }), + ], }); - expect(field.remappedField()).toBe(nameField); + + expect(field.remappedField()).toBeDefined(); + expect(field.remappedField()).toBe(field.metadata?.field(2)); }); it("should return null when the field has no name_field or no dimension with a 'human readable' field", () => { - expect(new Field().remappedField()).toBe(null); + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + }), + ], + }); + + expect(field.remappedField()).toBe(null); }); }); describe("remappedValue", () => { it("should call a given value using the instance's remapping property", () => { - const field = new Field({ - remapping: { - get: () => 1, - }, + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + remappings: [[2, "A"]], + }), + ], }); - expect(field.remappedValue(2)).toBe(1); + + expect(field.remappedValue(2)).toBe("A"); }); it("should convert a numeric field into a number if it is not a number", () => { - const field = new Field({ - isNumeric: () => true, - remapping: { - get: num => num, - }, + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + base_type: TYPE.Number, + semantic_type: TYPE.Number, + remappings: [[2.5, "A"]], + }), + ], }); - expect(field.remappedValue("2.5rem")).toBe(2.5); + + expect(field.remappedValue("2.5rem")).toBe("A"); }); }); describe("hasRemappedValue", () => { it("should call a given value using the instance's remapping property", () => { - const field = new Field({ - remapping: { - has: () => true, - }, + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + remappings: [[2, "A"]], + }), + ], }); - expect(field.hasRemappedValue(2)).toBe(true); + + expect(field.hasRemappedValue(1)).toBeFalsy(); + expect(field.hasRemappedValue(2)).toBeTruthy(); }); - it("should convert a numeric field into a number if it is not a number", () => { - const field = new Field({ - isNumeric: () => true, - remapping: { - has: num => typeof num === "number", - }, + it("should not convert a numeric field into a number if it is not a number", () => { + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + remappings: [[2.5, "A"]], + }), + ], }); - expect(field.hasRemappedValue("2.5rem")).toBe(true); + + expect(field.remappedValue("2.5rem")).toBeFalsy(); }); }); describe("isSearchable", () => { it("should be true when the field is a string", () => { - const field = new Field({ - isString: () => true, + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + base_type: TYPE.Text, + semantic_type: TYPE.Text, + }), + ], }); + expect(field.isSearchable()).toBe(true); }); + it("should be false when the field is not a string", () => { - const field = new Field({ - isString: () => false, + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + base_type: TYPE.Number, + semantic_type: TYPE.Number, + }), + ], }); + expect(field.isSearchable()).toBe(false); }); }); describe("fieldValues", () => { it("should return the values on a field instance", () => { - const values = [[1], [2]]; - const field = new Field({ - values, + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + values: [[1], [2]], + }), + ], }); - expect(field.fieldValues()).toEqual(values); + + expect(field.fieldValues()).toEqual([[1], [2]]); }); it("should wrap raw values in arrays to match the format of remapped values", () => { - const values = [1, 2]; - const field = new Field({ - values, + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + values: [[1], [2]], + }), + ], }); + expect(field.fieldValues()).toEqual([[1], [2]]); }); }); describe("hasFieldValues", () => { it("should be true when a field has values", () => { - expect( - new Field({ - values: [1], - }).hasFieldValues(), - ).toBe(true); - }); + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + values: [[1], [2]], + }), + ], + }); - it("should be false when a field has no values", () => { - expect( - new Field({ - values: [], - }).hasFieldValues(), - ).toBe(false); - expect( - new Field({ - values: undefined, - }).hasFieldValues(), - ).toBe(false); + expect(field.hasFieldValues()).toBe(true); }); - }); - describe("getUniqueId", () => { - describe("when the `uniqueId` field exists on the instance", () => { - it("should return the `uniqueId`", () => { - const field = new Field({ - uniqueId: "foo", - }); - expect(field.getUniqueId()).toBe("foo"); + it("should be false when a field has empty values", () => { + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + values: [], + }), + ], }); - }); - describe("when the `uniqueId` field does not exist on the instance of a concrete Field", () => { - let field; - beforeEach(() => { - field = createMockConcreteField({ - apiOpts: { - id: 1, - table_id: 2, - }, - }); - }); + expect(field.hasFieldValues()).toBe(false); + }); - it("should create a `uniqueId`", () => { - expect(field.getUniqueId()).toBe(1); + it("should be false when a field has no values", () => { + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + }), + ], }); - it("should set the `uniqueId` on the Field instance", () => { - field.getUniqueId(); - expect(field.uniqueId).toBe(1); - }); + expect(field.hasFieldValues()).toBe(false); }); + }); - describe("when the `uniqueId` field does not exist on the instance of a Field from a virtual card Table", () => { - let field; - beforeEach(() => { - field = createMockConcreteField({ - apiOpts: { - id: 1, - table_id: "card__123", - }, + describe("getUniqueId", () => { + describe("when the `uniqueId` field exists on the instance", () => { + it("should return the `uniqueId`", () => { + const field = setup({ + fields: [ + createMockField({ + id: FIELD_ID, + }), + ], }); - }); - - it("should create a `uniqueId`", () => { - expect(field.getUniqueId()).toBe("card__123:1"); - }); - it("should set the `uniqueId` on the Field instance", () => { - field.getUniqueId(); - expect(field.uniqueId).toBe("card__123:1"); + expect(field.getUniqueId()).toBe(1); }); }); }); diff --git a/frontend/src/metabase-lib/metadata/Metadata.ts b/frontend/src/metabase-lib/metadata/Metadata.ts index 9023f331902bbe6273bc85e61cab004c6eee5789..9c2cad94892117fc1fbc72b77ec896cd1d73c271 100644 --- a/frontend/src/metabase-lib/metadata/Metadata.ts +++ b/frontend/src/metabase-lib/metadata/Metadata.ts @@ -94,7 +94,7 @@ class Metadata { } field( - fieldId: FieldId | FieldReference | undefined | null, + fieldId: FieldId | FieldReference | string | undefined | null, tableId?: TableId | undefined | null, ): Field | null { if (fieldId == null) { diff --git a/frontend/src/metabase-lib/metadata/Metadata.unit.spec.ts b/frontend/src/metabase-lib/metadata/Metadata.unit.spec.ts index 347d16ede7712a130a9910de1d88c0dafd59627b..4fe9a7fbcdbf309d2b315f850e5cdf6eaf0dfa65 100644 --- a/frontend/src/metabase-lib/metadata/Metadata.unit.spec.ts +++ b/frontend/src/metabase-lib/metadata/Metadata.unit.spec.ts @@ -1,249 +1,139 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck -import Metadata from "./Metadata"; -import Database from "./Database"; -import Table from "./Table"; -import Schema from "./Schema"; -import Segment from "./Segment"; -import Metric from "./Metric"; -import { createMockConcreteField } from "./mocks"; +import { createMockMetadata } from "__support__/metadata"; +import { + createMockDatabase, + createMockField, + createMockMetric, + createMockSegment, + createMockTable, +} from "metabase-types/api/mocks"; describe("Metadata", () => { describe("instantiation", () => { it("should create an instance of Metadata", () => { - expect(new Metadata()).toBeInstanceOf(Metadata); - }); - it("should add `object` props to the instance", () => { - expect( - new Metadata({ - foo: "bar", - }), - ).toHaveProperty("foo", "bar"); + const metadata = createMockMetadata({}); + expect(metadata).toBeDefined(); }); }); + describe("databasesList (deprecated)", () => { - let databases; - let databaseA; - let databaseB; - let databaseC; - beforeEach(() => { - databaseA = new Database({ - id: 2, - name: "A", - is_saved_questions: true, - }); - databaseB = new Database({ - id: 3, - name: "B", - }); - databaseC = new Database({ - id: 1, - name: "C", - }); - databases = { - 1: databaseC, - 2: databaseA, - 3: databaseB, - }; + const metadata = createMockMetadata({ + databases: [ + createMockDatabase({ + id: 2, + name: "A", + is_saved_questions: true, + }), + createMockDatabase({ + id: 3, + name: "B", + }), + createMockDatabase({ + id: 1, + name: "C", + }), + ], }); + it("should return a sorted list of database objects found on the metadata instance", () => { - const metadata = new Metadata({ - databases, - }); - expect(metadata.databasesList()).toEqual([ - databaseA, - databaseB, - databaseC, + const databases = metadata.databasesList(); + expect(databases).toEqual([ + metadata.database(2), + metadata.database(3), + metadata.database(1), ]); }); + it("should return all databases when the `savedQuestions` flag is true", () => { - const metadata = new Metadata({ - databases, - }); - expect( - metadata.databasesList({ - savedQuestions: true, - }), - ).toEqual(metadata.databasesList()); + const databases = metadata.databasesList({ savedQuestions: true }); + expect(databases).toEqual([ + metadata.database(2), + metadata.database(3), + metadata.database(1), + ]); }); + it("should exclude the 'is_saved_questions' db when the `savedQuestions` flag is false", () => { - const metadata = new Metadata({ - databases, - }); - expect( - metadata.databasesList({ - savedQuestions: false, - }), - ).toEqual([databaseB, databaseC]); + const databases = metadata.databasesList({ savedQuestions: false }); + expect(databases).toEqual([metadata.database(3), metadata.database(1)]); }); }); + describe("tablesList (deprecated)", () => { it("should return a list of table objects found on the instance", () => { - const tableA = new Table({ - id: 1, - name: "A", - }); - const tableB = new Table({ - id: 2, - name: "B", - }); - const tables = { - 1: tableA, - 2: tableB, - }; - const metadata = new Metadata({ - tables, + const metadata = createMockMetadata({ + tables: [createMockTable({ id: 1 }), createMockTable({ id: 2 })], }); - expect(metadata.tablesList()).toEqual([tableA, tableB]); + + const tables = metadata.tablesList(); + expect(tables).toEqual([metadata.table(1), metadata.table(2)]); }); }); describe("metricsList (deprecated)", () => { it("should return a list of metric objects found on the instance", () => { - const metricA = new Metric({ - id: 1, - name: "A", - }); - const metricB = new Metric({ - id: 2, - name: "B", + const metadata = createMockMetadata({ + metrics: [createMockMetric({ id: 1 }), createMockMetric({ id: 2 })], }); - const metrics = { - 1: metricA, - 2: metricB, - }; - const metadata = new Metadata({ - metrics, - }); - expect(metadata.metricsList()).toEqual([metricA, metricB]); + + const metrics = metadata.metricsList(); + expect(metrics).toEqual([metadata.metric(1), metadata.metric(2)]); }); }); describe("segmentsList (deprecated)", () => { it("should return a list of segment objects found on the instance", () => { - const segmentA = new Segment({ - id: 1, - name: "A", + const metadata = createMockMetadata({ + segments: [createMockSegment({ id: 1 }), createMockSegment({ id: 2 })], }); - const segmentB = new Segment({ - id: 2, - name: "B", - }); - const segments = { - 1: segmentA, - 2: segmentB, - }; - const metadata = new Metadata({ - segments, - }); - expect(metadata.segmentsList()).toEqual([segmentA, segmentB]); - }); - }); - [ - ["segment", obj => new Segment(obj)], - ["metric", obj => new Metric(obj)], - ["database", obj => new Database(obj)], - ["schema", obj => new Schema(obj)], - ["table", obj => new Table(obj)], - ].forEach(([fnName, instantiate]) => { - describe(fnName, () => { - let instanceA; - let instanceB; - let metadata; - beforeEach(() => { - instanceA = instantiate({ - id: 1, - name: "A", - }); - instanceB = instantiate({ - id: 2, - name: "B", - }); - const instances = { - 1: instanceA, - 2: instanceB, - }; - metadata = new Metadata({ - [`${fnName}s`]: instances, - }); - }); - it(`should retun the ${fnName} with the given id`, () => { - expect(metadata[fnName](1)).toBe(instanceA); - expect(metadata[fnName](2)).toBe(instanceB); - }); - it("should return null when the id matches nothing", () => { - expect(metadata[fnName](3)).toBeNull(); - }); - it("should return null when the id is nil", () => { - expect(metadata[fnName]()).toBeNull(); - }); + const segments = metadata.segmentsList(); + expect(segments).toEqual([metadata.segment(1), metadata.segment(2)]); }); }); describe("`field`", () => { it("should return null when given a nil fieldId arg", () => { - const metadata = new Metadata({ - fields: {}, - }); - expect(metadata.field()).toBeNull(); + const metadata = createMockMetadata({}); expect(metadata.field(null)).toBeNull(); }); describe("when given a fieldId and no tableId", () => { it("should return null when there is no matching field", () => { - const metadata = new Metadata({ - fields: {}, - }); + const metadata = createMockMetadata({}); expect(metadata.field(1)).toBeNull(); }); it("should return the matching Field instance", () => { - const field = createMockConcreteField({ apiOpts: { id: 1 } }); - const uniqueId = field.getUniqueId(); - const metadata = new Metadata({ - fields: { - [uniqueId]: field, - }, + const metadata = createMockMetadata({ + fields: [createMockField({ id: 1 })], }); - expect(metadata.field(1)).toBe(field); + expect(metadata.field(1)).toBeDefined(); }); }); describe("when given a fieldId and a concrete tableId", () => { it("should ignore the tableId arg because these fields are stored using the field's id", () => { - const field = createMockConcreteField({ - apiOpts: { id: 1, table_id: 1 }, - }); - const uniqueId = field.getUniqueId(); - const metadata = new Metadata({ - fields: { - [uniqueId]: field, - }, + const metadata = createMockMetadata({ + fields: [createMockField({ id: 1, table_id: 1 })], }); + const field = metadata.field(1); + expect(field).toBeDefined(); expect(metadata.field(1, 1)).toBe(field); - // to prove the point that the `tableId` is ignore in this scenario + // to prove the point that the `tableId` is ignored in this scenario expect(metadata.field(1, 2)).toBe(field); - expect(metadata.field(1)).toBe(field); }); }); describe("when given a fieldId and a virtual card tableId", () => { it("should return the matching Field instance, stored using the field's `uniqueId`", () => { - const field = createMockConcreteField({ - apiOpts: { id: 1, table_id: "card__123" }, - }); - const uniqueId = field.getUniqueId(); - const metadata = new Metadata({ - fields: { - [uniqueId]: field, - }, + const metadata = createMockMetadata({ + fields: [createMockField({ id: 1, table_id: "card__123" })], }); - expect(metadata.field(1, "card__123")).toBe(field); + const field = metadata.field(1, "card__123"); + expect(field).toBeDefined(); expect(metadata.field("card__123:1")).toBe(field); - - expect(metadata.field(1)).not.toBe(field); + expect(metadata.field(1)).toBeNull(); }); }); }); diff --git a/frontend/src/metabase-lib/metadata/Schema.unit.spec.ts b/frontend/src/metabase-lib/metadata/Schema.unit.spec.ts index bc6845e8e87fd82ca0caddc08a64aec53f191a60..5ec3ed7edd05ced1a8d858a7287e03c686ef2cdd 100644 --- a/frontend/src/metabase-lib/metadata/Schema.unit.spec.ts +++ b/frontend/src/metabase-lib/metadata/Schema.unit.spec.ts @@ -1,27 +1,29 @@ -import Schema from "./Schema"; +import { Table } from "metabase-types/api"; +import { createMockTable } from "metabase-types/api/mocks"; +import { createMockMetadata } from "__support__/metadata"; + +const TEST_TABLE = createMockTable({ + schema: "foo_bar", +}); + +interface SetupOpts { + table?: Table; +} + +const setup = ({ table = TEST_TABLE }: SetupOpts = {}) => { + const metadata = createMockMetadata({ tables: [table] }); + const instance = metadata.table(table.id)?.schema; + if (!instance) { + throw new TypeError(); + } + + return instance; +}; describe("Schema", () => { - describe("instantiation", () => { - it("should create an instance of Schema", () => { - expect(new Schema({ id: "1:public", name: "public" })).toBeInstanceOf( - Schema, - ); - }); - it("should add `object` props to the instance", () => { - expect( - new Schema({ - id: "1:public", - name: "public", - }), - ).toHaveProperty("name", "public"); - }); - }); describe("displayName", () => { it("should return a formatted `name` string", () => { - const schema = new Schema({ - id: "name: public", - name: "foo_bar", - }); + const schema = setup(); expect(schema.displayName()).toBe("Foo Bar"); }); }); diff --git a/frontend/src/metabase-lib/metadata/Segment.unit.spec.ts b/frontend/src/metabase-lib/metadata/Segment.unit.spec.ts index 56332969d54897f4937ffac04ec6b79b8a700af2..d84c64b6e2cbcedb552a18c1c78949ee1277b42e 100644 --- a/frontend/src/metabase-lib/metadata/Segment.unit.spec.ts +++ b/frontend/src/metabase-lib/metadata/Segment.unit.spec.ts @@ -1,45 +1,75 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck -import Segment from "./Segment"; +import { Segment } from "metabase-types/api"; +import { createMockSegment } from "metabase-types/api/mocks"; +import { createMockMetadata } from "__support__/metadata"; + +interface SetupOpts { + segment?: Segment; +} + +const setup = ({ segment = createMockSegment() }: SetupOpts = {}) => { + const metadata = createMockMetadata({ + segments: [segment], + }); + + const instance = metadata.segment(segment.id); + if (!instance) { + throw new TypeError(); + } + + return instance; +}; describe("Segment", () => { describe("instantiation", () => { it("should create an instance of Segment", () => { - expect(new Segment()).toBeInstanceOf(Segment); + const segment = setup(); + expect(segment).toBeDefined(); }); }); + describe("displayName", () => { it("should return the `name` property found on the instance", () => { - expect( - new Segment({ + const segment = setup({ + segment: createMockSegment({ name: "foo", - }).displayName(), - ).toBe("foo"); + }), + }); + + expect(segment.displayName()).toBe("foo"); }); }); + describe("filterClause", () => { it("should return a filter clause", () => { - expect( - new Segment({ + const segment = setup({ + segment: createMockSegment({ id: 123, - }).filterClause(), - ).toEqual(["segment", 123]); + }), + }); + + expect(segment.filterClause()).toEqual(["segment", 123]); }); }); + describe("isActive", () => { it("should return true if the segment is not archived", () => { - expect( - new Segment({ + const segment = setup({ + segment: createMockSegment({ archived: false, - }).isActive(), - ).toBe(true); + }), + }); + + expect(segment.isActive()).toBe(true); }); + it("should return false if the segment is archived", () => { - expect( - new Segment({ + const segment = setup({ + segment: createMockSegment({ archived: true, - }).isActive(), - ).toBe(false); + }), + }); + + expect(segment.isActive()).toBe(false); }); }); }); diff --git a/frontend/src/metabase-lib/metadata/utils/fields.ts b/frontend/src/metabase-lib/metadata/utils/fields.ts index 6b28120d460e2f351fe12f01551900df733eb760..8e4abb907a36524538a260865cf7e7e57736358e 100644 --- a/frontend/src/metabase-lib/metadata/utils/fields.ts +++ b/frontend/src/metabase-lib/metadata/utils/fields.ts @@ -1,3 +1,4 @@ +import { FieldId, FieldReference, TableId } from "metabase-types/api"; import { isVirtualCardId } from "metabase-lib/metadata/utils/saved-questions"; import { BOOLEAN, @@ -11,7 +12,6 @@ import { TEMPORAL, } from "metabase-lib/types/constants"; import { getFieldType } from "metabase-lib/types/utils/isa"; -import type Field from "../Field"; const ICON_MAPPING: Record<string, string> = { [TEMPORAL]: "calendar", @@ -33,11 +33,16 @@ export function getIconForField(fieldOrColumn: any) { return type && ICON_MAPPING[type] ? ICON_MAPPING[type] : "unknown"; } -export function getUniqueFieldId( - field: Pick<Field, "id" | "name" | "table_id">, -): number | string { - const { table_id } = field; - const fieldIdentifier = getFieldIdentifier(field); +export function getUniqueFieldId({ + id, + name, + table_id, +}: { + id: FieldId | FieldReference | string; + name?: string | undefined | null; + table_id?: TableId | undefined | null; +}): number | string { + const fieldIdentifier = getFieldIdentifier({ id, name }); if (isVirtualCardId(table_id)) { return `${table_id}:${fieldIdentifier}`; @@ -46,13 +51,16 @@ export function getUniqueFieldId( return fieldIdentifier; } -function getFieldIdentifier( - field: Pick<Field, "id" | "name">, -): number | string { - const { id, name } = field; +function getFieldIdentifier({ + id, + name, +}: { + id: FieldId | FieldReference | string; + name?: string | undefined | null; +}): number | string { if (Array.isArray(id)) { return id[1]; } - return id || name; + return id ?? name; } diff --git a/frontend/src/metabase-lib/metadata/utils/models.ts b/frontend/src/metabase-lib/metadata/utils/models.ts index 089cd03cc566fba76d037c02a67ef3192c767e06..e7bd5b572c0250f8ef5ff665c1c8f9d75c4c1f12 100644 --- a/frontend/src/metabase-lib/metadata/utils/models.ts +++ b/frontend/src/metabase-lib/metadata/utils/models.ts @@ -15,7 +15,7 @@ import { isSameField } from "metabase-lib/queries/utils/field-ref"; import { isStructured } from "metabase-lib/queries/utils"; type FieldMetadata = { - id?: number; + id?: number | string; name: string; display_name: string; description?: string | null; diff --git a/frontend/src/metabase-types/api/field.ts b/frontend/src/metabase-types/api/field.ts index 6030e4867a5c854152f5310aba25c55f91563290..c67e159824f757cc4d9f1d141bf1f6b7b6803f1b 100644 --- a/frontend/src/metabase-types/api/field.ts +++ b/frontend/src/metabase-types/api/field.ts @@ -55,6 +55,7 @@ export type FieldValuesType = "list" | "search" | "none"; export type FieldDimension = { name: string; + human_readable_field_id?: FieldId; human_readable_field?: Field; }; @@ -86,11 +87,13 @@ export interface ConcreteField { fk_target_field_id: FieldId | null; target?: Field; values?: FieldValue[]; + remappings?: FieldValue[]; settings?: FieldFormattingSettings; dimensions?: FieldDimension[]; default_dimension_option?: FieldDimensionOption; dimension_options?: FieldDimensionOption[]; + name_field?: Field; max_value?: number; min_value?: number; diff --git a/frontend/src/metabase-types/api/schema.ts b/frontend/src/metabase-types/api/schema.ts index d4e99d923b1f6364a94552598aed2ae807d7ea87..44fd91b80f34c57c6055744b8369874a7654f2d6 100644 --- a/frontend/src/metabase-types/api/schema.ts +++ b/frontend/src/metabase-types/api/schema.ts @@ -41,6 +41,7 @@ export interface NormalizedTable segments?: SegmentId[]; metrics?: MetricId[]; schema?: SchemaId; + schema_name?: string; } export interface NormalizedForeignKey diff --git a/frontend/src/metabase-types/api/table.ts b/frontend/src/metabase-types/api/table.ts index cc8f9c6f1269aea0b542998040aaffacd4c001fb..7191a38751216003be5858e02240b87eb6fff43d 100644 --- a/frontend/src/metabase-types/api/table.ts +++ b/frontend/src/metabase-types/api/table.ts @@ -30,7 +30,6 @@ export interface Table { db_id: DatabaseId; db?: Database; - schema_name?: string; schema: string; fks?: ForeignKey[]; diff --git a/frontend/test/metabase-lib/lib/queries/StructuredQuery-nesting.unit.spec.js b/frontend/test/metabase-lib/lib/queries/StructuredQuery-nesting.unit.spec.js index ffc3df7be3b47c04ecc8d6716746deb0ef2e2fcf..4ae2df0e6e07bc7af30a071e38db27e690d34619 100644 --- a/frontend/test/metabase-lib/lib/queries/StructuredQuery-nesting.unit.spec.js +++ b/frontend/test/metabase-lib/lib/queries/StructuredQuery-nesting.unit.spec.js @@ -1,23 +1,32 @@ -import { createMockMetadata } from "__support__/metadata"; +import { createMockField, createMockTable } from "metabase-types/api/mocks"; import { + createOrdersTable, + createProductsTable, createSampleDatabase, ORDERS, ORDERS_ID, - PRODUCTS_ID, PEOPLE_ID, + PRODUCTS_ID, } from "metabase-types/api/mocks/presets"; +import { createMockMetadata } from "__support__/metadata"; -const metadata = createMockMetadata({ - databases: [createSampleDatabase()], -}); +const setup = ({ tables = [] } = {}) => { + const metadata = createMockMetadata({ + databases: [createSampleDatabase()], + tables, + }); -const ordersTable = metadata.table(ORDERS_ID); -const productsTable = metadata.table(PRODUCTS_ID); -const peopleTable = metadata.table(PEOPLE_ID); + return { + ordersTable: metadata.table(ORDERS_ID), + productsTable: metadata.table(PRODUCTS_ID), + peopleTable: metadata.table(PEOPLE_ID), + }; +}; describe("StructuredQuery nesting", () => { describe("nest", () => { it("should nest correctly", () => { + const { ordersTable } = setup(); const q = ordersTable.query(); expect(q.query()).toEqual({ "source-table": ORDERS_ID }); expect(q.nest().query()).toEqual({ @@ -26,6 +35,7 @@ describe("StructuredQuery nesting", () => { }); it("should be able to modify the outer question", () => { + const { ordersTable } = setup(); const q = ordersTable.query(); expect( q @@ -39,6 +49,7 @@ describe("StructuredQuery nesting", () => { }); it("should be able to modify the source question", () => { + const { ordersTable } = setup(); const q = ordersTable.query(); expect( q @@ -56,6 +67,7 @@ describe("StructuredQuery nesting", () => { }); it("should return a table with correct dimensions", () => { + const { ordersTable } = setup(); const q = ordersTable .query() .aggregate(["count"]) @@ -74,6 +86,7 @@ describe("StructuredQuery nesting", () => { describe("topLevelFilters", () => { it("should return filters for the last two stages", () => { + const { ordersTable } = setup(); const q = ordersTable .query() .aggregate(["count"]) @@ -93,12 +106,14 @@ describe("StructuredQuery nesting", () => { describe("topLevelQuery", () => { it("should return the query if it's summarized", () => { + const { ordersTable } = setup(); const q = ordersTable.query(); expect(q.topLevelQuery().query()).toEqual({ "source-table": ORDERS_ID, }); }); it("should return the query if it's not summarized", () => { + const { ordersTable } = setup(); const q = ordersTable.query().aggregate(["count"]); expect(q.topLevelQuery().query()).toEqual({ "source-table": ORDERS_ID, @@ -106,12 +121,14 @@ describe("StructuredQuery nesting", () => { }); }); it("should return last stage if none are summarized", () => { + const { ordersTable } = setup(); const q = ordersTable.query().nest(); expect(q.topLevelQuery().query()).toEqual({ "source-query": { "source-table": ORDERS_ID }, }); }); it("should return last summarized stage if any is summarized", () => { + const { ordersTable } = setup(); const q = ordersTable.query().aggregate(["count"]).nest(); expect(q.topLevelQuery().query()).toEqual({ "source-table": ORDERS_ID, @@ -122,6 +139,7 @@ describe("StructuredQuery nesting", () => { describe("topLevelDimension", () => { it("should return same dimension if not nested", () => { + const { ordersTable } = setup(); const q = ordersTable.query(); const d = q.topLevelDimension( q.parseFieldReference(["field", ORDERS.TOTAL, null]), @@ -129,6 +147,7 @@ describe("StructuredQuery nesting", () => { expect(d.mbql()).toEqual(["field", ORDERS.TOTAL, null]); }); it("should return underlying dimension for a nested query", () => { + const { ordersTable } = setup(); const q = ordersTable .query() .aggregate(["count"]) @@ -146,72 +165,40 @@ describe("StructuredQuery nesting", () => { }); describe("model question", () => { - let dataset; - let virtualCardTable; - beforeEach(() => { - const question = ordersTable.question(); - dataset = question.setId(123).setDataset(true); - - // create a virtual table for the card - // that contains fields from both Orders and Products tables - // to imitate an explicit join of Products to Orders - virtualCardTable = ordersTable.clone(); - virtualCardTable.id = `card__123`; - virtualCardTable.fields = virtualCardTable.fields - .map(f => - f.clone({ - table_id: `card__123`, - uniqueId: `card__123:${f.id}`, - }), - ) - .concat( - productsTable.fields.map(f => { - const field = f.clone({ - table_id: `card__123`, - uniqueId: `card__123:${f.id}`, - }); + it("should not include implicit join dimensions when the underyling question has an explicit join", () => { + const fields = [ + ...createOrdersTable().fields, + ...createProductsTable().fields, + ]; - return field; + const { ordersTable, productsTable, peopleTable } = setup({ + tables: [ + createMockTable({ + id: "card__1", + fields: fields.map(field => + createMockField({ ...field, table_id: "card__1" }), + ), }), - ); - - // add instances to the `metadata` instance - metadata.questions[dataset.id()] = dataset; - metadata.tables[virtualCardTable.id] = virtualCardTable; - virtualCardTable.fields.forEach(f => { - metadata.fields[f.uniqueId] = f; + ], }); - }); - it("should not include implicit join dimensions when the underyling question has an explicit join", () => { + const metadata = ordersTable.metadata; + const question = ordersTable.question(); + const dataset = question.setId(1).setDataset(true); const nestedDatasetQuery = dataset.composeDataset().query(); expect( // get a list of all dimension options for the nested query nestedDatasetQuery .dimensionOptions() .all() - .map(d => d.field().getPlainObject()), + .map(d => d.field()), ).toEqual([ // Order fields - ...ordersTable.fields.map(f => - f - .clone({ - table_id: `card__123`, - uniqueId: `card__123:${f.id}`, - }) - .getPlainObject(), - ), + ...ordersTable.fields.map(({ id }) => metadata.field(id, "card__1")), // Product fields from the explicit join - ...productsTable.fields.map(f => - f - .clone({ - table_id: `card__123`, - uniqueId: `card__123:${f.id}`, - }) - .getPlainObject(), - ), + ...productsTable.fields.map(({ id }) => metadata.field(id, "card__1")), // People fields from the implicit join - ...peopleTable.fields.map(f => f.getPlainObject()), + ...peopleTable.fields, ]); }); });