From 1a9b648d335e6f9b279c8cd962b3c8f61c75c3b8 Mon Sep 17 00:00:00 2001
From: Tom Robinson <tlrobinson@gmail.com>
Date: Fri, 10 Jul 2020 16:02:08 -0700
Subject: [PATCH] Metabase lib cleanup - metadata edition (#12859)

* Remove StructuredQuery's dependency on deprecated metabase/lib/query.js

* Refactor QueryDefinition{,Tooltip} to use metabase-lib

* Replace *Name components with displayName and various other legacy cleanup

* Fix flow

* Move flow types to metabase-types

* Remove loadTableAndForeignKeys

* Add aggregationOperator/filterOperator to Table/Field and remove metabase/lib/table.js completely

* Use metadata database/table/field/etc functions instead of property access

* Fix flow
---
 frontend/src/metabase-lib/README.md           |  24 ++--
 frontend/src/metabase-lib/lib/Dimension.js    |   2 +-
 frontend/src/metabase-lib/lib/Question.js     |  10 --
 .../src/metabase-lib/lib/metadata/Database.js |  21 ++++
 .../src/metabase-lib/lib/metadata/Field.js    |  74 +++++++++---
 .../src/metabase-lib/lib/metadata/Table.js    |  51 ++++++--
 .../metabase-lib/lib/queries/NativeQuery.js   |   2 +-
 .../lib/queries/StructuredQuery.js            |  24 +---
 .../lib/queries/structured/Aggregation.js     |  10 +-
 .../lib/queries/structured/Filter.js          |   2 +-
 frontend/src/metabase-lib/lib/utils.js        |   8 ++
 .../admin/datamodel/containers/FieldApp.jsx   |  30 ++---
 frontend/src/metabase/dashboard/selectors.js  |   4 +-
 frontend/src/metabase/entities/tables.js      |  11 +-
 frontend/src/metabase/lib/permissions.js      |   4 +-
 frontend/src/metabase/lib/query/field_ref.js  |  17 +--
 frontend/src/metabase/lib/table.js            |  85 -------------
 frontend/src/metabase/meta/Card.js            |  10 +-
 frontend/src/metabase/meta/Parameter.js       |   4 +-
 .../components/TimeseriesFilterWidget.jsx     |   5 +-
 .../modes/components/modes/TimeseriesMode.jsx |   2 -
 frontend/src/metabase/modes/lib/drilldown.js  |   2 +-
 .../components/AggregationPopover.jsx         |  17 +--
 .../query_builder/components/FieldList.jsx    |  15 ++-
 .../components/QueryVisualization.jsx         |   6 +-
 .../components/dataref/FieldPane.jsx          |   8 +-
 .../components/dataref/MetricPane.jsx         |   2 +-
 .../components/dataref/SegmentPane.jsx        |   2 +-
 .../components/dataref/TablePane.jsx          | 112 +++++++++---------
 .../components/filters/FilterWidgetList.jsx   |  19 +--
 .../template_tags/TagEditorParam.jsx          |   4 +-
 .../query_builder/containers/QueryBuilder.jsx |   3 -
 frontend/src/metabase/reference/utils.js      |   4 +-
 frontend/src/metabase/selectors/metadata.js   |  27 -----
 .../visualizations/ObjectDetail.jsx           |  21 ++--
 .../metabase-lib/lib/Dimension.unit.spec.js   |   6 +-
 .../containers/SaveQuestionModal.unit.spec.js |   2 +-
 .../lib/expressions/compile.unit.spec.js      |   2 +-
 frontend/test/metabase/meta/Card.unit.spec.js |  20 +++-
 .../modes/TimeseriesFilterWidget.unit.spec.js |   1 -
 .../metabase/selectors/metadata.unit.spec.js  |   2 +-
 41 files changed, 304 insertions(+), 371 deletions(-)
 delete mode 100644 frontend/src/metabase/lib/table.js

diff --git a/frontend/src/metabase-lib/README.md b/frontend/src/metabase-lib/README.md
index 253f8443a30..4979d46aa4f 100644
--- a/frontend/src/metabase-lib/README.md
+++ b/frontend/src/metabase-lib/README.md
@@ -1,23 +1,23 @@
 ## Wrapper Objects:
 
-* `setFoo(bar)`: returns clone of the wrapper but with the "foo" attribute to set to `bar`
-* `replace(object)`: returns clone of parent wrapper with this object replaced by `object`
-* `remove()`: returns clone of the parent wrapper with this object removed
-* `update()`: propagates current wrapper update to parent wrapper, recursively
+- `setFoo(bar)`: returns clone of the wrapper but with the "foo" attribute to set to `bar`
+- `replace(object)`: returns clone of parent wrapper with this object replaced by `object`
+- `remove()`: returns clone of the parent wrapper with this object removed
+- `update()`: propagates current wrapper update to parent wrapper, recursively
 
 Examples:
 
-* `question().query.aggregation()[0].setDimension(dimension).update()`
+- `question().query().aggregation()[0].setDimension(dimension).update()`
 
 Exceptions:
 
-* StructuredQuery::updateAggregation, updateBreakout, updateFilter, etc should be called setAggregation, etc
+- StructuredQuery::updateAggregation, updateBreakout, updateFilter, etc should be called setAggregation, etc
 
 ## Wrapper Hierarchy:
 
-* Question
-  * StructuredQuery
-    * Aggregation
-    * Breakout
-    * Filter
-  * NativeQuery
+- Question
+  - StructuredQuery
+    - Aggregation
+    - Breakout
+    - Filter
+  - NativeQuery
diff --git a/frontend/src/metabase-lib/lib/Dimension.js b/frontend/src/metabase-lib/lib/Dimension.js
index 4f3745f5476..3970e51866d 100644
--- a/frontend/src/metabase-lib/lib/Dimension.js
+++ b/frontend/src/metabase-lib/lib/Dimension.js
@@ -473,7 +473,7 @@ export class FieldIDDimension extends FieldDimension {
 
   field() {
     return (
-      (this._metadata && this._metadata.fields[this._args[0]]) ||
+      (this._metadata && this._metadata.field(this._args[0])) ||
       new Field({ id: this._args[0] })
     );
   }
diff --git a/frontend/src/metabase-lib/lib/Question.js b/frontend/src/metabase-lib/lib/Question.js
index d1b3a8dfc66..95febfeea59 100644
--- a/frontend/src/metabase-lib/lib/Question.js
+++ b/frontend/src/metabase-lib/lib/Question.js
@@ -627,16 +627,6 @@ export default class Question {
     }
   }
 
-  // deprecated
-  tableMetadata(): ?Table {
-    const query = this.query();
-    if (query instanceof StructuredQuery) {
-      return query.table();
-    } else {
-      return null;
-    }
-  }
-
   @memoize
   mode(): ?Mode {
     return Mode.forQuestion(this);
diff --git a/frontend/src/metabase-lib/lib/metadata/Database.js b/frontend/src/metabase-lib/lib/metadata/Database.js
index a81762abe46..b3bad7ac97d 100644
--- a/frontend/src/metabase-lib/lib/metadata/Database.js
+++ b/frontend/src/metabase-lib/lib/metadata/Database.js
@@ -6,6 +6,8 @@ import Base from "./Base";
 import Table from "./Table";
 import Schema from "./Schema";
 
+import { memoize, createLookupByProperty } from "metabase-lib/lib/utils";
+
 import { generateSchemaId } from "metabase/schema";
 
 import type { SchemaName } from "metabase-types/types/Table";
@@ -33,6 +35,8 @@ export default class Database extends Base {
     return this.name;
   }
 
+  // SCEMAS
+
   schema(schemaName: ?SchemaName) {
     return this.metadata.schema(generateSchemaId(this.id, schemaName));
   }
@@ -41,6 +45,21 @@ export default class Database extends Base {
     return this.schemas.map(s => s.name).sort((a, b) => a.localeCompare(b));
   }
 
+  // TABLES
+
+  @memoize
+  tablesLookup() {
+    return createLookupByProperty(this.tables, "id");
+  }
+
+  // @deprecated: use tablesLookup
+  // $FlowFixMe: known to not have side-effects
+  get tables_lookup() {
+    return this.tablesLookup();
+  }
+
+  // FEATURES
+
   hasFeature(
     feature: null | DatabaseFeature | VirtualDatabaseFeature,
   ): boolean {
@@ -60,6 +79,8 @@ export default class Database extends Base {
     }
   }
 
+  // QUESTIONS
+
   newQuestion(): Question {
     return this.question()
       .setDefaultQuery()
diff --git a/frontend/src/metabase-lib/lib/metadata/Field.js b/frontend/src/metabase-lib/lib/metadata/Field.js
index 0317eadbc7d..83b4bafff2f 100644
--- a/frontend/src/metabase-lib/lib/metadata/Field.js
+++ b/frontend/src/metabase-lib/lib/metadata/Field.js
@@ -5,6 +5,8 @@ import Table from "./Table";
 
 import moment from "moment";
 
+import { memoize, createLookupByProperty } from "metabase-lib/lib/utils";
+
 import Dimension from "../Dimension";
 
 import { formatField, stripId } from "metabase/lib/formatting";
@@ -48,7 +50,7 @@ export default class Field extends Base {
   name_field: ?Field;
 
   parent() {
-    return this.metadata ? this.metadata.fields[this.parent_id] : null;
+    return this.metadata ? this.metadata.field(this.parent_id) : null;
   }
 
   path() {
@@ -187,28 +189,70 @@ export default class Field extends Base {
     return d && d.field();
   }
 
+  // FILTERS
+
+  @memoize
+  filterOperators() {
+    return getFilterOperators(this, this.table);
+  }
+
+  @memoize
+  filterOperatorsLookup() {
+    return createLookupByProperty(this.filterOperators(), "name");
+  }
+
   filterOperator(operatorName) {
-    if (this.filter_operators_lookup) {
-      return this.filter_operators_lookup[operatorName];
-    } else {
-      return this.filterOperators().find(o => o.name === operatorName);
-    }
+    return this.filterOperatorsLookup()[operatorName];
   }
 
-  filterOperators() {
-    return this.filter_operators || getFilterOperators(this, this.table);
+  // @deprecated: use filterOperators
+  // $FlowFixMe: known to not have side-effects
+  get filter_operators() {
+    return this.filterOperators();
+  }
+  // @deprecated: use filterOperatorsLookup
+  // $FlowFixMe: known to not have side-effects
+  get filter_operators_lookup() {
+    return this.filterOperatorsLookup();
   }
 
+  // AGGREGATIONS
+
+  @memoize
   aggregationOperators() {
     return this.table
-      ? this.table.aggregation_operators.filter(
-          aggregation =>
-            aggregation.validFieldsFilters[0] &&
-            aggregation.validFieldsFilters[0]([this]).length === 1,
-        )
+      ? this.table
+          .aggregationOperators()
+          .filter(
+            aggregation =>
+              aggregation.validFieldsFilters[0] &&
+              aggregation.validFieldsFilters[0]([this]).length === 1,
+          )
       : null;
   }
 
+  @memoize
+  aggregationOperatorsLookup() {
+    return createLookupByProperty(this.aggregationOperators(), "short");
+  }
+
+  aggregationOperator(short) {
+    return this.aggregationOperatorsLookup()[short];
+  }
+
+  // @deprecated: use aggregationOperators
+  // $FlowFixMe: known to not have side-effects
+  get aggregation_operators() {
+    return this.aggregationOperators();
+  }
+  // @deprecated: use aggregationOperatorsLookup
+  // $FlowFixMe: known to not have side-effects
+  get aggregation_operators_lookup() {
+    return this.aggregationOperatorsLookup();
+  }
+
+  // BREAKOUTS
+
   /**
    * Returns a default breakout MBQL clause for this field
    */
@@ -240,6 +284,8 @@ export default class Field extends Base {
     }
   }
 
+  // REMAPPINGS
+
   /**
    * Returns the remapped field, if any
    */
@@ -247,7 +293,7 @@ export default class Field extends Base {
     const displayFieldId =
       this.dimensions && this.dimensions.human_readable_field_id;
     if (displayFieldId != null) {
-      return this.metadata.fields[displayFieldId];
+      return this.metadata.field(displayFieldId);
     }
     // this enables "implicit" remappings from type/PK to type/Name on the same table,
     // used in FieldValuesWidget, but not table/object detail listings
diff --git a/frontend/src/metabase-lib/lib/metadata/Table.js b/frontend/src/metabase-lib/lib/metadata/Table.js
index df0c25909d7..9a627f85ef4 100644
--- a/frontend/src/metabase-lib/lib/metadata/Table.js
+++ b/frontend/src/metabase-lib/lib/metadata/Table.js
@@ -8,19 +8,17 @@ import Database from "./Database";
 import Schema from "./Schema";
 import Field from "./Field";
 
-import type { SchemaName } from "metabase-types/types/Table";
-import type { FieldMetadata } from "metabase-types/types/Metadata";
+import Dimension from "../Dimension";
 
 import { singularize } from "metabase/lib/formatting";
+import { getAggregationOperatorsWithFields } from "metabase/lib/schema_metadata";
+import { memoize, createLookupByProperty } from "metabase-lib/lib/utils";
 
-import Dimension from "../Dimension";
-
+import type { SchemaName } from "metabase-types/types/Table";
 import type StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 
 type EntityType = string; // TODO: move somewhere central
 
-import _ from "underscore";
-
 /** This is the primary way people interact with tables */
 export default class Table extends Base {
   description: string;
@@ -31,7 +29,7 @@ export default class Table extends Base {
   // @deprecated: use schema.name (all tables should have a schema object, in theory)
   schema_name: ?SchemaName;
 
-  fields: FieldMetadata[];
+  fields: Field[];
 
   entity_type: ?EntityType;
 
@@ -91,11 +89,44 @@ export default class Table extends Base {
     return this.fields.filter(field => field.isDate());
   }
 
+  // AGGREGATIONS
+
+  @memoize
   aggregationOperators() {
-    return this.aggregation_operators || [];
+    return getAggregationOperatorsWithFields(this);
+  }
+
+  @memoize
+  aggregationOperatorsLookup() {
+    return createLookupByProperty(this.aggregationOperators(), "short");
+  }
+
+  aggregationOperator(short) {
+    return this.aggregation_operators_lookup[short];
+  }
+
+  // @deprecated: use aggregationOperators
+  // $FlowFixMe: known to not have side-effects
+  get aggregation_operators() {
+    return this.aggregationOperators();
+  }
+
+  // @deprecated: use aggregationOperatorsLookup
+  // $FlowFixMe: known to not have side-effects
+  get aggregation_operators_lookup() {
+    return this.aggregationOperatorsLookup();
+  }
+
+  // FIELDS
+
+  @memoize
+  fieldsLookup() {
+    return createLookupByProperty(this.fields, "id");
   }
 
-  aggregation(agg) {
-    return _.findWhere(this.aggregationOperators(), { short: agg });
+  // @deprecated: use fieldsLookup
+  // $FlowFixMe: known to not have side-effects
+  get fields_lookup() {
+    return this.fieldsLookup();
   }
 }
diff --git a/frontend/src/metabase-lib/lib/queries/NativeQuery.js b/frontend/src/metabase-lib/lib/queries/NativeQuery.js
index 8a41996d84d..70bb0656464 100644
--- a/frontend/src/metabase-lib/lib/queries/NativeQuery.js
+++ b/frontend/src/metabase-lib/lib/queries/NativeQuery.js
@@ -125,7 +125,7 @@ export default class NativeQuery extends AtomicQuery {
   }
   database(): ?Database {
     const databaseId = this.databaseId();
-    return databaseId != null ? this._metadata.databases[databaseId] : null;
+    return databaseId != null ? this._metadata.database(databaseId) : null;
   }
   engine(): ?DatabaseEngine {
     const database = this.database();
diff --git a/frontend/src/metabase-lib/lib/queries/StructuredQuery.js b/frontend/src/metabase-lib/lib/queries/StructuredQuery.js
index 2d8ccd6e945..b9402c62885 100644
--- a/frontend/src/metabase-lib/lib/queries/StructuredQuery.js
+++ b/frontend/src/metabase-lib/lib/queries/StructuredQuery.js
@@ -5,7 +5,6 @@
  */
 
 import * as Q from "metabase/lib/query/query";
-import { addValidOperatorsToFields } from "metabase/lib/schema_metadata";
 import {
   format as formatExpression,
   DISPLAY_QUOTES,
@@ -29,10 +28,7 @@ import type {
   DatasetQuery,
   StructuredDatasetQuery,
 } from "metabase-types/types/Card";
-import type {
-  TableMetadata,
-  AggregationOperator,
-} from "metabase-types/types/Metadata";
+import type { AggregationOperator } from "metabase-types/types/Metadata";
 
 import Dimension, {
   FKDimension,
@@ -59,7 +55,6 @@ import OrderByWrapper from "./structured/OrderBy";
 
 import Table from "../metadata/Table";
 import Field from "../metadata/Field";
-import { augmentDatabase } from "metabase/lib/table";
 
 import { TYPE } from "metabase/lib/types";
 
@@ -303,12 +298,12 @@ export default class StructuredQuery extends AtomicQuery {
   table(): Table {
     const sourceQuery = this.sourceQuery();
     if (sourceQuery) {
-      const table = new Table({
+      return new Table({
         name: "",
         display_name: "",
         db: sourceQuery.database(),
         fields: sourceQuery.columns().map(
-          (column, index) =>
+          column =>
             new Field({
               ...column,
               id: ["field-literal", column.name, column.base_type],
@@ -320,22 +315,11 @@ export default class StructuredQuery extends AtomicQuery {
         segments: [],
         metrics: [],
       });
-      // HACK: ugh various parts of the UI still expect this stuff
-      addValidOperatorsToFields(table);
-      augmentDatabase({ tables: [table] });
-      return table;
     } else {
       return this.metadata().table(this.sourceTableId());
     }
   }
 
-  /**
-   * @deprecated Alias of `table()`. Use only when partially porting old code that uses @type {TableMetadata} object.
-   */
-  tableMetadata(): ?TableMetadata {
-    return this.table();
-  }
-
   /**
    * Removes invalid clauses from the query (and source-query, recursively)
    */
@@ -634,7 +618,7 @@ export default class StructuredQuery extends AtomicQuery {
    */
   aggregationFieldOptions(agg: string | AggregationOperator): DimensionOptions {
     const aggregation: AggregationOperator =
-      typeof agg === "string" ? this.table().aggregation(agg) : agg;
+      typeof agg === "string" ? this.table().aggregationOperator(agg) : agg;
     if (aggregation) {
       const fieldOptions = this.fieldOptions(field => {
         return (
diff --git a/frontend/src/metabase-lib/lib/queries/structured/Aggregation.js b/frontend/src/metabase-lib/lib/queries/structured/Aggregation.js
index bb0ae779655..2ec65953e16 100644
--- a/frontend/src/metabase-lib/lib/queries/structured/Aggregation.js
+++ b/frontend/src/metabase-lib/lib/queries/structured/Aggregation.js
@@ -138,14 +138,14 @@ export default class Aggregation extends MBQLClause {
       return this.aggregation().isValid();
     } else if (this.isStandard() && this.dimension()) {
       const dimension = this.dimension();
-      const aggregation = this.query()
+      const aggregationOperator = this.query()
         .table()
-        .aggregation(this[0]);
+        .aggregationOperator(this[0]);
       return (
-        aggregation &&
-        (!aggregation.requiresField ||
+        aggregationOperator &&
+        (!aggregationOperator.requiresField ||
           this.query()
-            .aggregationFieldOptions(aggregation)
+            .aggregationFieldOptions(aggregationOperator)
             .hasDimension(dimension))
       );
     } else if (this.isMetric()) {
diff --git a/frontend/src/metabase-lib/lib/queries/structured/Filter.js b/frontend/src/metabase-lib/lib/queries/structured/Filter.js
index 2b7a7ddc3c3..07544edce75 100644
--- a/frontend/src/metabase-lib/lib/queries/structured/Filter.js
+++ b/frontend/src/metabase-lib/lib/queries/structured/Filter.js
@@ -224,7 +224,7 @@ export default class Filter extends MBQLClause {
   setDimension(
     fieldRef: ?Field,
     { useDefaultOperator = false }: { useDefaultOperator?: boolean } = {},
-  ) {
+  ): Filter {
     if (!fieldRef) {
       return this.set([]);
     }
diff --git a/frontend/src/metabase-lib/lib/utils.js b/frontend/src/metabase-lib/lib/utils.js
index 6cdacc70e41..8cc16b8ecda 100644
--- a/frontend/src/metabase-lib/lib/utils.js
+++ b/frontend/src/metabase-lib/lib/utils.js
@@ -56,3 +56,11 @@ export function sortObject(obj) {
   }
   return o;
 }
+
+export function createLookupByProperty(items, property) {
+  const lookup = {};
+  for (const item of items) {
+    lookup[item[property]] = item;
+  }
+  return lookup;
+}
diff --git a/frontend/src/metabase/admin/datamodel/containers/FieldApp.jsx b/frontend/src/metabase/admin/datamodel/containers/FieldApp.jsx
index cda9658ad3f..eb93593c6cd 100644
--- a/frontend/src/metabase/admin/datamodel/containers/FieldApp.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/FieldApp.jsx
@@ -195,8 +195,8 @@ export default class FieldApp extends React.Component {
       params: { section },
     } = this.props;
 
-    const db = metadata.databases[databaseId];
-    const table = metadata.tables[tableId];
+    const db = metadata.database(databaseId);
+    const table = metadata.table(tableId);
 
     const isLoading = !field || !table || !idfields;
 
@@ -224,18 +224,20 @@ export default class FieldApp extends React.Component {
             }
           >
             <div className="wrapper">
-              <div className="mb4 pt2 ml-auto mr-auto">
-                <Breadcrumbs
-                  crumbs={[
-                    [db.name, `/admin/datamodel/database/${db.id}`],
-                    [
-                      table.display_name,
-                      `/admin/datamodel/database/${db.id}/table/${table.id}`,
-                    ],
-                    t`${field.display_name} – Field Settings`,
-                  ]}
-                />
-              </div>
+              {db && table && (
+                <div className="mb4 pt2 ml-auto mr-auto">
+                  <Breadcrumbs
+                    crumbs={[
+                      [db.name, `/admin/datamodel/database/${db.id}`],
+                      [
+                        table.display_name,
+                        `/admin/datamodel/database/${db.id}/table/${table.id}`,
+                      ],
+                      t`${field.display_name} – Field Settings`,
+                    ]}
+                  />
+                </div>
+              )}
               <div className="absolute top right mt4 mr4">
                 <SaveStatus ref={ref => (this.saveStatus = ref)} />
               </div>
diff --git a/frontend/src/metabase/dashboard/selectors.js b/frontend/src/metabase/dashboard/selectors.js
index 846adf3a075..d5f9d522f13 100644
--- a/frontend/src/metabase/dashboard/selectors.js
+++ b/frontend/src/metabase/dashboard/selectors.js
@@ -123,7 +123,7 @@ export const getMappingsByParameter = createSelector(
         const card = _.findWhere(cards, { id: mapping.card_id });
         const fieldId =
           card && getParameterTargetFieldId(mapping.target, card.dataset_query);
-        const field = metadata.fields[fieldId];
+        const field = metadata.field(fieldId);
         const values = (field && field.fieldValues()) || [];
         if (values.length) {
           countsByParameter[mapping.parameter_id] =
@@ -210,7 +210,7 @@ export const getParameters = createSelector(
         .filter(fieldId => fieldId != null)
         .value();
       const fieldIdsWithFKResolved = _.chain(fieldIds)
-        .map(id => metadata.fields[id])
+        .map(id => metadata.field(id))
         .filter(f => f)
         .map(f => (f.target || f).id)
         .uniq()
diff --git a/frontend/src/metabase/entities/tables.js b/frontend/src/metabase/entities/tables.js
index 6886a96ec87..fe3bdc216c6 100644
--- a/frontend/src/metabase/entities/tables.js
+++ b/frontend/src/metabase/entities/tables.js
@@ -22,8 +22,6 @@ import Fields from "metabase/entities/fields";
 
 import { GET, PUT } from "metabase/lib/api";
 
-import { addValidOperatorsToFields } from "metabase/lib/schema_metadata";
-
 import { getMetadata } from "metabase/selectors/metadata";
 
 const listTables = GET("/api/table");
@@ -78,13 +76,12 @@ const Tables = createEntity({
         ({ id }) => [...Tables.getObjectStatePath(id), "fetchMetadata"],
       ),
       withNormalize(TableSchema),
-    )(({ id }, options = {}) => async (dispatch, getState) => {
-      const table = await MetabaseApi.table_query_metadata({
+    )(({ id }, options = {}) => (dispatch, getState) =>
+      MetabaseApi.table_query_metadata({
         tableId: id,
         ...options.params,
-      });
-      return addValidOperatorsToFields(table);
-    }),
+      }),
+    ),
 
     // like fetchMetadata but also loads tables linked by foreign key
     fetchMetadataAndForeignTables: createThunkAction(
diff --git a/frontend/src/metabase/lib/permissions.js b/frontend/src/metabase/lib/permissions.js
index 1e4b27522ad..5f77dd29a19 100644
--- a/frontend/src/metabase/lib/permissions.js
+++ b/frontend/src/metabase/lib/permissions.js
@@ -200,7 +200,7 @@ function inferEntityPermissionValueFromChildTables(
   metadata: Metadata,
 ) {
   const { databaseId } = entityId;
-  const database = metadata && metadata.databases[databaseId];
+  const database = metadata && metadata.database(databaseId);
 
   const entityIdsForDescendantTables: TableEntityId[] = _.chain(database.tables)
     .filter(t => _.isMatch(t, entityIdToMetadataTableFields(entityId)))
@@ -342,7 +342,7 @@ export function updateSchemasPermission(
   value: string,
   metadata: Metadata,
 ): GroupsPermissions {
-  const database = metadata.databases[databaseId];
+  const database = metadata.database(databaseId);
   const schemaNames = database && database.schemaNames();
   const schemaNamesOrNoSchema =
     schemaNames &&
diff --git a/frontend/src/metabase/lib/query/field_ref.js b/frontend/src/metabase/lib/query/field_ref.js
index 33792369fe4..c8304f40aee 100644
--- a/frontend/src/metabase/lib/query/field_ref.js
+++ b/frontend/src/metabase/lib/query/field_ref.js
@@ -3,9 +3,7 @@ import _ from "underscore";
 import Field from "metabase-lib/lib/metadata/Field";
 import * as Table from "./table";
 
-import { getFilterOperators } from "metabase/lib/schema_metadata";
 import { TYPE } from "metabase/lib/types";
-import { createLookupByProperty } from "metabase/lib/table";
 
 // DEPRECATED
 export function isRegularField(field: FieldReference): boolean {
@@ -126,24 +124,11 @@ export function getFieldTarget(field, tableDef, path = []) {
       display_name: field[1],
       name: field[1],
       expression_name: field[1],
+      table: tableDef,
       metadata: tableDef.metadata,
       // TODO: we need to do something better here because filtering depends on knowing a sensible type for the field
       base_type: TYPE.Float,
-      filter_operators_lookup: {},
-      filter_operators: [],
-      active: true,
-      fk_target_field_id: null,
-      parent_id: null,
-      preview_display: true,
-      special_type: null,
-      target: null,
-      visibility_type: "normal",
     });
-    fieldDef.filter_operators = getFilterOperators(fieldDef, tableDef);
-    fieldDef.filter_operators_lookup = createLookupByProperty(
-      fieldDef.filter_operators,
-      "name",
-    );
 
     return {
       table: tableDef,
diff --git a/frontend/src/metabase/lib/table.js b/frontend/src/metabase/lib/table.js
deleted file mode 100644
index 7650fccb7c4..00000000000
--- a/frontend/src/metabase/lib/table.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import { addValidOperatorsToFields } from "metabase/lib/schema_metadata";
-
-import _ from "underscore";
-
-import { MetabaseApi } from "metabase/services";
-
-export async function loadTableAndForeignKeys(tableId) {
-  const [table, foreignKeys] = await Promise.all([
-    MetabaseApi.table_query_metadata({ tableId }),
-    MetabaseApi.table_fks({ tableId }),
-  ]);
-
-  await augmentTable(table);
-
-  return {
-    table,
-    foreignKeys,
-  };
-}
-
-export async function augmentTable(table) {
-  table = populateQueryOptions(table);
-  table = await loadForeignKeyTables(table);
-  return table;
-}
-
-export function augmentDatabase(database) {
-  database.tables_lookup = createLookupByProperty(database.tables, "id");
-  for (const table of database.tables) {
-    addValidOperatorsToFields(table);
-    table.fields_lookup = createLookupByProperty(table.fields, "id");
-    for (const field of table.fields) {
-      addFkTargets(field, database.tables_lookup);
-      field.filter_operators_lookup = createLookupByProperty(
-        field.filter_operators,
-        "name",
-      );
-    }
-  }
-  return database;
-}
-
-async function loadForeignKeyTables(table) {
-  // Load joinable tables
-  await Promise.all(
-    table.fields
-      .filter(f => f.target != null)
-      .map(async field => {
-        const targetTable = await MetabaseApi.table_query_metadata({
-          tableId: field.target.table_id,
-        });
-        field.target.table = populateQueryOptions(targetTable);
-      }),
-  );
-  return table;
-}
-
-function populateQueryOptions(table) {
-  table = addValidOperatorsToFields(table);
-
-  table.fields_lookup = {};
-
-  _.each(table.fields, function(field) {
-    table.fields_lookup[field.id] = field;
-    field.filter_operators_lookup = {};
-    _.each(field.filter_operators, function(operator) {
-      field.filter_operators_lookup[operator.name] = operator;
-    });
-  });
-
-  return table;
-}
-
-function addFkTargets(field, tables) {
-  if (field.target != null) {
-    field.target.table = tables[field.target.table_id];
-  }
-}
-
-export function createLookupByProperty(items, property) {
-  return items.reduce((lookup, item) => {
-    lookup[item[property]] = item;
-    return lookup;
-  }, {});
-}
diff --git a/frontend/src/metabase/meta/Card.js b/frontend/src/metabase/meta/Card.js
index 55dc15a3c78..257d5345b02 100644
--- a/frontend/src/metabase/meta/Card.js
+++ b/frontend/src/metabase/meta/Card.js
@@ -26,7 +26,8 @@ import type {
   ParameterMapping,
   ParameterValues,
 } from "metabase-types/types/Parameter";
-import type { Metadata, TableMetadata } from "metabase-types/types/Metadata";
+import type Metadata from "metabase-lib/lib/metadata/Metadata";
+import type Table from "metabase-lib/lib/metadata/Table";
 
 declare class Object {
   static values<T>(object: { [key: string]: T }): Array<T>;
@@ -97,13 +98,10 @@ export function getQuery(card: Card): ?StructuredQuery {
   }
 }
 
-export function getTableMetadata(
-  card: Card,
-  metadata: Metadata,
-): ?TableMetadata {
+export function getTableMetadata(card: Card, metadata: Metadata): ?Table {
   const query = getQuery(card);
   if (query && query["source-table"] != null) {
-    return metadata.tables[query["source-table"]] || null;
+    return metadata.table(query["source-table"]) || null;
   }
   return null;
 }
diff --git a/frontend/src/metabase/meta/Parameter.js b/frontend/src/metabase/meta/Parameter.js
index 93dc13114da..eeaa6792540 100644
--- a/frontend/src/metabase/meta/Parameter.js
+++ b/frontend/src/metabase/meta/Parameter.js
@@ -17,7 +17,7 @@ import type {
   ParameterType,
 } from "metabase-types/types/Parameter";
 import type { FieldId } from "metabase-types/types/Field";
-import type { Metadata } from "metabase-types/types/Metadata";
+import type Metadata from "metabase-lib/lib/metadata/Metadata";
 
 import moment from "moment";
 
@@ -212,7 +212,7 @@ export function parameterToMBQLFilter(
     return dateParameterValueToMBQL(parameter.value, fieldRef);
   } else {
     const fieldId = FIELD_REF.getFieldTargetId(fieldRef);
-    const field = metadata.fields[fieldId];
+    const field = metadata.field(fieldId);
     // if the field is numeric, parse the value as a number
     if (isNumericBaseType(field)) {
       return numberParameterValueToMBQL(parameter.value, fieldRef);
diff --git a/frontend/src/metabase/modes/components/TimeseriesFilterWidget.jsx b/frontend/src/metabase/modes/components/TimeseriesFilterWidget.jsx
index 32d630d8a01..62bf5c1b805 100644
--- a/frontend/src/metabase/modes/components/TimeseriesFilterWidget.jsx
+++ b/frontend/src/metabase/modes/components/TimeseriesFilterWidget.jsx
@@ -25,13 +25,11 @@ import type {
   Card as CardObject,
   StructuredDatasetQuery,
 } from "metabase-types/types/Card";
-import type { TableMetadata } from "metabase-types/types/Metadata";
 import type { FieldFilter } from "metabase-types/types/Query";
 
 type Props = {
   className?: string,
   card: CardObject,
-  tableMetadata: TableMetadata,
   setDatasetQuery: (
     datasetQuery: StructuredDatasetQuery,
     options: { run: boolean },
@@ -88,7 +86,7 @@ export default class TimeseriesFilterWidget extends Component {
   }
 
   render() {
-    const { className, card, tableMetadata, setDatasetQuery } = this.props;
+    const { className, card, setDatasetQuery } = this.props;
     const { filter, filterIndex, currentFilter } = this.state;
     let currentDescription;
 
@@ -126,7 +124,6 @@ export default class TimeseriesFilterWidget extends Component {
           onFilterChange={newFilter => {
             this.setState({ filter: newFilter });
           }}
-          tableMetadata={tableMetadata}
           includeAllTime
         />
         <div className="p1">
diff --git a/frontend/src/metabase/modes/components/modes/TimeseriesMode.jsx b/frontend/src/metabase/modes/components/modes/TimeseriesMode.jsx
index e64be7869b4..0fec4972e93 100644
--- a/frontend/src/metabase/modes/components/modes/TimeseriesMode.jsx
+++ b/frontend/src/metabase/modes/components/modes/TimeseriesMode.jsx
@@ -16,12 +16,10 @@ import type {
   Card as CardObject,
   DatasetQuery,
 } from "metabase-types/types/Card";
-import type { TableMetadata } from "metabase-types/types/Metadata";
 import TimeseriesGroupingWidget from "metabase/modes/components/TimeseriesGroupingWidget";
 
 type Props = {
   lastRunCard: CardObject,
-  tableMetadata: TableMetadata,
   setDatasetQuery: (datasetQuery: DatasetQuery) => void,
   runQuestionQuery: () => void,
 };
diff --git a/frontend/src/metabase/modes/lib/drilldown.js b/frontend/src/metabase/modes/lib/drilldown.js
index 8fef2fc48b4..8105223f3a4 100644
--- a/frontend/src/metabase/modes/lib/drilldown.js
+++ b/frontend/src/metabase/modes/lib/drilldown.js
@@ -254,6 +254,6 @@ function columnToBreakout(column) {
 // returns the table metadata for a dimension
 function tableForDimensions(dimensions, metadata) {
   const fieldId = getIn(dimensions, [0, "column", "id"]);
-  const field = metadata.fields[fieldId];
+  const field = metadata.field(fieldId);
   return field && field.table;
 }
diff --git a/frontend/src/metabase/query_builder/components/AggregationPopover.jsx b/frontend/src/metabase/query_builder/components/AggregationPopover.jsx
index 47b7b430f5d..02f36932861 100644
--- a/frontend/src/metabase/query_builder/components/AggregationPopover.jsx
+++ b/frontend/src/metabase/query_builder/components/AggregationPopover.jsx
@@ -139,11 +139,6 @@ export default class AggregationPopover extends Component {
     });
   };
 
-  _getTableMetadata() {
-    const { query, tableMetadata } = this.props;
-    return tableMetadata || query.tableMetadata();
-  }
-
   _getAvailableAggregations() {
     const { aggregationOperators, query, dimension, showRawData } = this.props;
     return (
@@ -193,14 +188,14 @@ export default class AggregationPopover extends Component {
       alwaysExpanded,
     } = this.props;
 
-    const tableMetadata = this._getTableMetadata();
+    const table = query.table();
     const aggregationOperators = this._getAvailableAggregations();
 
     if (dimension) {
       showCustom = false;
       showMetrics = false;
     }
-    if (tableMetadata.db.features.indexOf("expression-aggregations") < 0) {
+    if (table.database.hasFeature("expression-aggregations")) {
       showCustom = false;
     }
 
@@ -209,7 +204,7 @@ export default class AggregationPopover extends Component {
 
     let selectedAggregation;
     if (AGGREGATION.isMetric(aggregation)) {
-      selectedAggregation = _.findWhere(tableMetadata.metrics, {
+      selectedAggregation = _.findWhere(table.metrics, {
         id: AGGREGATION.getMetric(aggregation),
       });
     } else if (AGGREGATION.isStandard(aggregation)) {
@@ -231,8 +226,8 @@ export default class AggregationPopover extends Component {
 
     // we only want to consider active metrics, with the ONE exception that if the currently selected aggregation is a
     // retired metric then we include it in the list to maintain continuity
-    const metrics = tableMetadata.metrics
-      ? tableMetadata.metrics.filter(metric =>
+    const metrics = table.metrics
+      ? table.metrics.filter(metric =>
           showMetrics
             ? !metric.archived ||
               (selectedAggregation && selectedAggregation.id === metric.id)
@@ -349,7 +344,7 @@ export default class AggregationPopover extends Component {
             className={"text-green"}
             width={this.props.width}
             maxHeight={this.props.maxHeight - (this.state.headerHeight || 0)}
-            table={tableMetadata}
+            query={query}
             field={fieldId}
             fieldOptions={query.aggregationFieldOptions(agg)}
             onFieldChange={this.onPickField}
diff --git a/frontend/src/metabase/query_builder/components/FieldList.jsx b/frontend/src/metabase/query_builder/components/FieldList.jsx
index 7334b126a69..21691406a08 100644
--- a/frontend/src/metabase/query_builder/components/FieldList.jsx
+++ b/frontend/src/metabase/query_builder/components/FieldList.jsx
@@ -7,11 +7,9 @@ import DimensionList from "./DimensionList";
 import Dimension from "metabase-lib/lib/Dimension";
 import DimensionOptions from "metabase-lib/lib/DimensionOptions";
 
-import type {
-  StructuredQuery,
-  ConcreteField,
-} from "metabase-types/types/Query";
+import type { ConcreteField } from "metabase-types/types/Query";
 import type Metadata from "metabase-lib/lib/metadata/Metadata";
+import type StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 
 // import type { Section } from "metabase/components/AccordionList";
 export type AccordionListItem = {};
@@ -87,11 +85,16 @@ export default class FieldList extends Component {
   };
 
   render() {
-    const { field, metadata, query } = this.props;
+    const { field, query, metadata } = this.props;
+    const dimension =
+      field &&
+      (query
+        ? query.parseFieldReference(field)
+        : Dimension.parseMBQL(field, metadata));
     return (
       <DimensionList
         sections={this.state.sections}
-        dimension={field && Dimension.parseMBQL(field, metadata, query)}
+        dimension={dimension}
         onChangeDimension={this.handleChangeDimension}
         onChangeOther={this.handleChangeOther}
         // forward AccordionList props
diff --git a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
index 7ba03db0513..1b751d6a5b8 100644
--- a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
@@ -15,8 +15,8 @@ import Utils from "metabase/lib/utils";
 import cx from "classnames";
 
 import Question from "metabase-lib/lib/Question";
-import type { Database } from "metabase-types/types/Database";
-import type { TableMetadata } from "metabase-types/types/Metadata";
+import type Database from "metabase-lib/lib/metadata/Database";
+import type Table from "metabase-lib/lib/metadata/Table";
 import type { DatasetQuery } from "metabase-types/types/Card";
 
 import type { ParameterValues } from "metabase-types/types/Parameter";
@@ -26,7 +26,7 @@ type Props = {
   originalQuestion: Question,
   result?: Object,
   databases?: Database[],
-  tableMetadata?: TableMetadata,
+  tableMetadata?: Table,
   tableForeignKeys?: [],
   tableForeignKeyReferences?: {},
   onUpdateVisualizationSettings: any => void,
diff --git a/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx b/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx
index 8f5d795e05f..4782cf433c7 100644
--- a/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx
@@ -78,7 +78,7 @@ export default class FieldPane extends Component {
         query = query.clearAggregations();
       }
 
-      const defaultBreakout = metadata.fields[field.id].getDefaultBreakout();
+      const defaultBreakout = metadata.field(field.id).getDefaultBreakout();
       query = query.breakout(defaultBreakout);
 
       this.props.updateQuestion(query.question());
@@ -89,7 +89,7 @@ export default class FieldPane extends Component {
   newCard = () => {
     const { metadata, field } = this.props;
     const tableId = field.table_id;
-    const dbId = metadata.tables[tableId].database.id;
+    const dbId = metadata.table(tableId).database.id;
 
     const card = createCard();
     card.dataset_query = Q_DEPRECATED.createQuery("query", dbId, tableId);
@@ -104,7 +104,7 @@ export default class FieldPane extends Component {
 
   setQueryDistinct = () => {
     const { metadata, field } = this.props;
-    const defaultBreakout = metadata.fields[field.id].getDefaultBreakout();
+    const defaultBreakout = metadata.field(field.id).getDefaultBreakout();
 
     const card = this.newCard();
     card.dataset_query.query.aggregation = ["rows"];
@@ -114,7 +114,7 @@ export default class FieldPane extends Component {
 
   setQueryCountGroupedBy = chartType => {
     const { metadata, field } = this.props;
-    const defaultBreakout = metadata.fields[field.id].getDefaultBreakout();
+    const defaultBreakout = metadata.field(field.id).getDefaultBreakout();
 
     const card = this.newCard();
     card.dataset_query.query.aggregation = ["count"];
diff --git a/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx b/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx
index fca11bfe882..daa2484046c 100644
--- a/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx
@@ -50,7 +50,7 @@ export default class MetricPane extends Component {
 
   newCard() {
     const { metric, metadata } = this.props;
-    const table = metadata && metadata.tables[metric.table_id];
+    const table = metadata && metadata.table(metric.table_id);
 
     if (table) {
       const card = createCard();
diff --git a/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx b/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx
index 4cf803b2ea0..6b18b06862b 100644
--- a/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx
@@ -76,7 +76,7 @@ export default class SegmentPane extends Component {
 
   newCard() {
     const { segment, metadata } = this.props;
-    const table = metadata && metadata.tables[segment.table_id];
+    const table = metadata && metadata.table(segment.table_id);
 
     if (table) {
       const card = createCard();
diff --git a/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx b/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx
index ce9cc73e7c3..94e65eb6234 100644
--- a/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx
@@ -1,85 +1,85 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component } from "react";
+import React from "react";
 import PropTypes from "prop-types";
+import { connect } from "react-redux";
 import { t } from "ttag";
 import cx from "classnames";
-import Icon from "metabase/components/Icon";
 
 // components
+import Icon from "metabase/components/Icon";
 import Expandable from "metabase/components/Expandable";
 
 // lib
-import { createCard } from "metabase/lib/card";
-import * as Q_DEPRECATED from "metabase/lib/query";
 import { foreignKeyCountsByOriginTable } from "metabase/lib/schema_metadata";
 import { inflect } from "metabase/lib/formatting";
 
-export default class TablePane extends Component {
-  constructor(props, context) {
-    super(props, context);
-    this.setQueryAllRows = this.setQueryAllRows.bind(this);
-    this.showPane = this.showPane.bind(this);
-
-    this.state = {
-      table: undefined,
-      tableForeignKeys: undefined,
-      pane: "fields",
-    };
-  }
+// entities
+import Table from "metabase/entities/tables";
+
+const mapStateToProps = (state, ownProps) => ({
+  tableId: ownProps.table.id,
+  table: Table.selectors.getObject(state, { entityId: ownProps.table.id }),
+});
+
+const mapDispatchToProps = {
+  fetchForeignKeys: Table.actions.fetchForeignKeys,
+  fetchMetadata: Table.actions.fetchMetadata,
+};
+
+@connect(
+  mapStateToProps,
+  mapDispatchToProps,
+)
+export default class TablePane extends React.Component {
+  state = {
+    pane: "fields",
+    error: null,
+  };
 
   static propTypes = {
     query: PropTypes.object.isRequired,
-    loadTableAndForeignKeysFn: PropTypes.func.isRequired,
     show: PropTypes.func.isRequired,
     onClose: PropTypes.func.isRequired,
     setCardAndRun: PropTypes.func.isRequired,
+    tableId: PropTypes.number.isRequired,
     table: PropTypes.object,
+    fetchForeignKeys: PropTypes.func.isRequired,
+    fetchMetadata: PropTypes.func.isRequired,
   };
 
-  componentWillMount() {
-    this.props
-      .loadTableAndForeignKeysFn(this.props.table.id)
-      .then(result => {
-        this.setState({
-          table: result.table,
-          tableForeignKeys: result.foreignKeys,
-        });
-      })
-      .catch(error => {
-        this.setState({
-          error: t`An error occurred loading the table`,
-        });
+  async componentWillMount() {
+    try {
+      await Promise.all([
+        this.props.fetchForeignKeys({ id: this.props.tableId }),
+        this.props.fetchMetadata({ id: this.props.tableId }),
+      ]);
+    } catch (e) {
+      this.setState({
+        error: t`An error occurred loading the table`,
       });
+    }
   }
 
-  showPane(name) {
+  showPane = name => {
     this.setState({ pane: name });
-  }
-
-  setQueryAllRows() {
-    const card = createCard();
-    card.dataset_query = Q_DEPRECATED.createQuery(
-      "query",
-      this.state.table.db_id,
-      this.state.table.id,
-    );
-    this.props.setCardAndRun(card);
-  }
+  };
 
   render() {
-    const { table, error } = this.state;
+    const { table } = this.props;
+    const { pane, error } = this.state;
     if (table) {
+      const fks = table.fks || [];
       const panes = {
         fields: table.fields.length,
         // "metrics": table.metrics.length,
         // "segments": table.segments.length,
-        connections: this.state.tableForeignKeys.length,
+        connections: fks.length,
       };
       const tabs = Object.entries(panes).map(([name, count]) => (
         <a
           key={name}
           className={cx("Button Button--small", {
-            "Button--active": name === this.state.pane,
+            "Button--active": name === pane,
           })}
           onClick={this.showPane.bind(null, name)}
         >
@@ -88,20 +88,18 @@ export default class TablePane extends Component {
         </a>
       ));
 
-      let pane;
       const descriptionClasses = cx({ "text-medium": !table.description });
       const description = (
         <p className={"text-spaced " + descriptionClasses}>
           {table.description || t`No description set.`}
         </p>
       );
-      if (this.state.pane === "connections") {
-        const fkCountsByTable = foreignKeyCountsByOriginTable(
-          this.state.tableForeignKeys,
-        );
-        pane = (
+      let content;
+      if (pane === "connections") {
+        const fkCountsByTable = foreignKeyCountsByOriginTable(fks);
+        content = (
           <ul>
-            {this.state.tableForeignKeys
+            {fks
               .sort((a, b) =>
                 a.origin.table.display_name.localeCompare(
                   b.origin.table.display_name,
@@ -126,11 +124,11 @@ export default class TablePane extends Component {
               ))}
           </ul>
         );
-      } else if (this.state.pane) {
-        const itemType = this.state.pane.replace(/s$/, "");
-        pane = (
+      } else if (pane) {
+        const itemType = pane.replace(/s$/, "");
+        content = (
           <ul>
-            {table[this.state.pane].map((item, index) => (
+            {table[pane].map((item, index) => (
               <li>
                 <a
                   key={item.id}
@@ -157,7 +155,7 @@ export default class TablePane extends Component {
               {tabs}
             </div>
           </div>
-          {pane}
+          {content}
         </div>
       );
     } else {
diff --git a/frontend/src/metabase/query_builder/components/filters/FilterWidgetList.jsx b/frontend/src/metabase/query_builder/components/filters/FilterWidgetList.jsx
index 4dc3fff1b3e..ba46e64d53d 100644
--- a/frontend/src/metabase/query_builder/components/filters/FilterWidgetList.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/FilterWidgetList.jsx
@@ -1,15 +1,12 @@
 /* @flow */
 
-import React, { Component } from "react";
+import React from "react";
 import { findDOMNode } from "react-dom";
 import { t } from "ttag";
 import FilterWidget from "./FilterWidget";
 
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 import Filter from "metabase-lib/lib/queries/structured/Filter";
-import Dimension from "metabase-lib/lib/Dimension";
-
-import type { TableMetadata } from "metabase-types/types/Metadata";
 
 type Props = {
   query: StructuredQuery,
@@ -17,14 +14,13 @@ type Props = {
   removeFilter?: (index: number) => void,
   updateFilter?: (index: number, filter: Filter) => void,
   maxDisplayValues?: number,
-  tableMetadata?: TableMetadata, // legacy parameter
 };
 
 type State = {
   shouldScroll: boolean,
 };
 
-export default class FilterList extends Component {
+export default class FilterWidgetList extends React.Component {
   props: Props;
   state: State;
 
@@ -55,21 +51,14 @@ export default class FilterList extends Component {
   }
 
   render() {
-    const { query, filters, tableMetadata } = this.props;
+    const { query, filters } = this.props;
     return (
       <div className="Query-filterList scroll-x scroll-show">
         {filters.map((filter, index) => (
           <FilterWidget
             key={index}
             placeholder={t`Item`}
-            // TODO: update widgets that are still passing tableMetadata instead of query
-            query={
-              query || {
-                table: () => tableMetadata,
-                parseFieldReference: fieldRef =>
-                  Dimension.parseMBQL(fieldRef, tableMetadata),
-              }
-            }
+            query={query}
             filter={filter}
             index={index}
             removeFilter={this.props.removeFilter}
diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx
index 6c05ed9fadf..68a9f7d2cdc 100644
--- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx
+++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx
@@ -83,7 +83,7 @@ export default class TagEditorParam extends Component {
     const { tag, onUpdate, metadata } = this.props;
     const dimension = ["field-id", fieldId];
     if (!_.isEqual(tag.dimension !== dimension)) {
-      const field = metadata.fields[dimension[1]];
+      const field = metadata.field(dimension[1]);
       if (!field) {
         return;
       }
@@ -111,7 +111,7 @@ export default class TagEditorParam extends Component {
       table,
       fieldMetadataLoaded = false;
     if (tag.type === "dimension" && Array.isArray(tag.dimension)) {
-      const field = metadata.fields[tag.dimension[1]];
+      const field = metadata.field(tag.dimension[1]);
 
       if (field) {
         widgetOptions = parameterOptionsForField(field);
diff --git a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
index 3e97346fd96..d7964205fa7 100644
--- a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
+++ b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
@@ -6,8 +6,6 @@ import { connect } from "react-redux";
 import { t } from "ttag";
 import _ from "underscore";
 
-import { loadTableAndForeignKeys } from "metabase/lib/table";
-
 import fitViewport from "metabase/hoc/FitViewPort";
 
 import View from "../components/view/View";
@@ -137,7 +135,6 @@ const mapStateToProps = (state, props) => {
     questionAlerts: getQuestionAlerts(state),
     visualizationSettings: getVisualizationSettings(state),
 
-    loadTableAndForeignKeysFn: loadTableAndForeignKeys,
     autocompleteResultsFn: prefix => autocompleteResults(state.qb.card, prefix),
     instanceSettings: getSettings(state),
 
diff --git a/frontend/src/metabase/reference/utils.js b/frontend/src/metabase/reference/utils.js
index cf419ec582a..60b892a2b3b 100644
--- a/frontend/src/metabase/reference/utils.js
+++ b/frontend/src/metabase/reference/utils.js
@@ -75,8 +75,8 @@ export const getQuestion = ({
     )
     .updateIn(["display"], display => visualization || display)
     .updateIn(["dataset_query", "query", "breakout"], oldBreakout => {
-      if (fieldId && metadata && metadata.fields[fieldId]) {
-        return [metadata.fields[fieldId].getDefaultBreakout()];
+      if (fieldId && metadata && metadata.field(fieldId)) {
+        return [metadata.field(fieldId).getDefaultBreakout()];
       }
       if (fieldId) {
         return [["field-id", fieldId]];
diff --git a/frontend/src/metabase/selectors/metadata.js b/frontend/src/metabase/selectors/metadata.js
index b76e8052a2e..d5d89d62599 100644
--- a/frontend/src/metabase/selectors/metadata.js
+++ b/frontend/src/metabase/selectors/metadata.js
@@ -18,10 +18,6 @@ import _ from "underscore";
 import { shallowEqual } from "recompose";
 import { getFieldValues, getRemappings } from "metabase/lib/query/field";
 
-import {
-  getFilterOperators,
-  getAggregationOperatorsWithFields,
-} from "metabase/lib/schema_metadata";
 import { getIn } from "icepick";
 
 // fully nomalized, raw "entities"
@@ -149,21 +145,9 @@ export const getMetadata = createSelector(
       }
     });
 
-    hydrate(meta.fields, "filter_operators", f =>
-      getFilterOperators(f, f.table),
-    );
-    hydrate(meta.tables, "aggregation_operators", t =>
-      getAggregationOperatorsWithFields(t),
-    );
-
     hydrate(meta.fields, "values", f => getFieldValues(f));
     hydrate(meta.fields, "remapping", f => new Map(getRemappings(f)));
 
-    hydrateLookup(meta.databases, "tables", "id");
-    hydrateLookup(meta.tables, "fields", "id");
-    hydrateLookup(meta.fields, "filter_operators", "name");
-    hydrateLookup(meta.tables, "aggregation_operators", "short");
-
     return meta;
   },
 );
@@ -310,17 +294,6 @@ function hydrateList(objects, property, targetObjects) {
   );
 }
 
-// creates a *_lookup object for a previously hydrated list
-function hydrateLookup(objects, property, idProperty = "id") {
-  hydrate(objects, property + "_lookup", object => {
-    const lookup = {};
-    for (const item of object[property] || []) {
-      lookup[item[idProperty]] = item;
-    }
-    return lookup;
-  });
-}
-
 function filterValues(obj, pred) {
   const filtered = {};
   for (const [k, v] of Object.entries(obj)) {
diff --git a/frontend/src/metabase/visualizations/visualizations/ObjectDetail.jsx b/frontend/src/metabase/visualizations/visualizations/ObjectDetail.jsx
index 26d5c298b1e..34a34cf3794 100644
--- a/frontend/src/metabase/visualizations/visualizations/ObjectDetail.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/ObjectDetail.jsx
@@ -15,7 +15,7 @@ import {
   foreignKeyCountsByOriginTable,
 } from "metabase/lib/schema_metadata";
 import { TYPE, isa } from "metabase/lib/types";
-import { singularize, inflect } from "inflection";
+import { inflect } from "inflection";
 import { formatValue, formatColumn } from "metabase/lib/formatting";
 
 import Tables from "metabase/entities/tables";
@@ -37,8 +37,8 @@ import cx from "classnames";
 import _ from "underscore";
 
 import type { VisualizationProps } from "metabase-types/types/Visualization";
-import type { TableMetadata } from "metabase-types/types/Metadata";
 import type { FieldId, Field } from "metabase-types/types/Field";
+import type Table from "metabase-lib/lib/metadata/Table";
 
 type ForeignKeyId = number;
 type ForeignKey = {
@@ -56,7 +56,7 @@ type ForeignKeyCountInfo = {
 };
 
 type Props = VisualizationProps & {
-  tableMetadata: ?TableMetadata,
+  table: ?Table,
   tableForeignKeys: ?(ForeignKey[]),
   tableForeignKeyReferences: { [id: ForeignKeyId]: ForeignKeyCountInfo },
   fetchTableFks: () => void,
@@ -68,7 +68,7 @@ type Props = VisualizationProps & {
 };
 
 const mapStateToProps = state => ({
-  tableMetadata: getTableMetadata(state),
+  table: getTableMetadata(state),
   tableForeignKeys: getTableForeignKeys(state),
   tableForeignKeyReferences: getTableForeignKeyReferences(state),
 });
@@ -99,9 +99,9 @@ export class ObjectDetail extends Component {
   };
 
   componentDidMount() {
-    const { tableMetadata } = this.props;
-    if (tableMetadata && tableMetadata.fks == null) {
-      this.props.fetchTableFks(tableMetadata.id);
+    const { table } = this.props;
+    if (table && table.fks == null) {
+      this.props.fetchTableFks(table.id);
     }
     // load up FK references
     if (this.props.tableForeignKeys) {
@@ -323,13 +323,12 @@ export class ObjectDetail extends Component {
   };
 
   render() {
-    if (!this.props.data) {
+    const { data, table } = this.props;
+    if (!data) {
       return false;
     }
 
-    const tableName = this.props.tableMetadata
-      ? singularize(this.props.tableMetadata.display_name)
-      : t`Unknown`;
+    const tableName = table ? table.objectName() : t`Unknown`;
     // TODO: once we nail down the "title" column of each table this should be something other than the id
     const idValue = this.getIdValue();
 
diff --git a/frontend/test/metabase-lib/lib/Dimension.unit.spec.js b/frontend/test/metabase-lib/lib/Dimension.unit.spec.js
index dfaa971aaa8..caa52feb2a2 100644
--- a/frontend/test/metabase-lib/lib/Dimension.unit.spec.js
+++ b/frontend/test/metabase-lib/lib/Dimension.unit.spec.js
@@ -223,7 +223,7 @@ describe("Dimension", () => {
         it("should return array of FK dimensions for foreign key field dimension", () => {
           pending();
           // Something like this:
-          // fieldsInProductsTable = metadata.tables[1].fields.length;
+          // fieldsInProductsTable = metadata.table(1).fields.length;
           // expect(FKDimension.dimensions(fkFieldIdDimension).length).toEqual(fieldsInProductsTable);
         });
         it("should return empty array for non-FK field dimension", () => {
@@ -286,7 +286,7 @@ describe("Dimension", () => {
         it("should return an array with dimensions for each datetime unit", () => {
           pending();
           // Something like this:
-          // fieldsInProductsTable = metadata.tables[1].fields.length;
+          // fieldsInProductsTable = metadata.table(1).fields.length;
           // expect(FKDimension.dimensions(fkFieldIdDimension).length).toEqual(fieldsInProductsTable);
         });
         it("should return empty array for non-date field dimension", () => {
@@ -421,7 +421,7 @@ describe("Dimension", () => {
         it("should return array of FK dimensions for foreign key field dimension", () => {
           pending();
           // Something like this:
-          // fieldsInProductsTable = metadata.tables[1].fields.length;
+          // fieldsInProductsTable = metadata.table(1).fields.length;
           // expect(FKDimension.dimensions(fkFieldIdDimension).length).toEqual(fieldsInProductsTable);
         });
         it("should return empty array for non-FK field dimension", () => {
diff --git a/frontend/test/metabase/containers/SaveQuestionModal.unit.spec.js b/frontend/test/metabase/containers/SaveQuestionModal.unit.spec.js
index d7393659789..68b0241d620 100644
--- a/frontend/test/metabase/containers/SaveQuestionModal.unit.spec.js
+++ b/frontend/test/metabase/containers/SaveQuestionModal.unit.spec.js
@@ -24,7 +24,7 @@ const mountSaveQuestionModal = (question, originalQuestion) => {
       <SaveQuestionModal
         card={question.card()}
         originalCard={originalQuestion && originalQuestion.card()}
-        tableMetadata={question.tableMetadata()}
+        tableMetadata={question.table()}
         onCreate={onCreateMock}
         onSave={onSaveMock}
         onClose={() => {}}
diff --git a/frontend/test/metabase/lib/expressions/compile.unit.spec.js b/frontend/test/metabase/lib/expressions/compile.unit.spec.js
index 2ca70856374..887e5991c23 100644
--- a/frontend/test/metabase/lib/expressions/compile.unit.spec.js
+++ b/frontend/test/metabase/lib/expressions/compile.unit.spec.js
@@ -6,7 +6,7 @@ import {
   expressionOpts,
 } from "./__support__/expressions";
 
-const ENABLE_PERF_TESTS = !process.env["CI"];
+const ENABLE_PERF_TESTS = false; //!process.env["CI"];
 
 function expectFast(fn, milliseconds = 1000) {
   const start = Date.now();
diff --git a/frontend/test/metabase/meta/Card.unit.spec.js b/frontend/test/metabase/meta/Card.unit.spec.js
index c01fa57e0c4..19a818335fe 100644
--- a/frontend/test/metabase/meta/Card.unit.spec.js
+++ b/frontend/test/metabase/meta/Card.unit.spec.js
@@ -1,16 +1,24 @@
 import * as Card from "metabase/meta/Card";
 
 import { assocIn, dissoc } from "icepick";
+import { getMetadata } from "metabase/selectors/metadata";
 
 describe("metabase/meta/Card", () => {
   describe("questionUrlWithParameters", () => {
-    const metadata = {
-      fields: {
-        2: {
-          base_type: "type/Integer",
+    const metadata = getMetadata({
+      entities: {
+        databases: {},
+        schemas: {},
+        tables: {},
+        fields: {
+          2: {
+            base_type: "type/Integer",
+          },
         },
+        metrics: {},
+        segments: {},
       },
-    };
+    });
 
     const parameters = [
       {
@@ -164,7 +172,7 @@ describe("metabase/meta/Card", () => {
           card,
           metadata,
           parameters,
-          { "2": "123" },
+          { "2": 123 },
           parameterMappings,
         );
         expect(parseUrl(url)).toEqual({
diff --git a/frontend/test/metabase/modes/TimeseriesFilterWidget.unit.spec.js b/frontend/test/metabase/modes/TimeseriesFilterWidget.unit.spec.js
index 8d0ab14bf09..ad11c3d4032 100644
--- a/frontend/test/metabase/modes/TimeseriesFilterWidget.unit.spec.js
+++ b/frontend/test/metabase/modes/TimeseriesFilterWidget.unit.spec.js
@@ -13,7 +13,6 @@ import {
 const getTimeseriesFilterWidget = question => (
   <TimeseriesFilterWidget
     card={question.card()}
-    tableMetadata={question.tableMetadata()}
     datasetQuery={question.query().datasetQuery()}
     setDatasetQuery={() => {}}
   />
diff --git a/frontend/test/metabase/selectors/metadata.unit.spec.js b/frontend/test/metabase/selectors/metadata.unit.spec.js
index fe95d4db5c3..79d7cb2583f 100644
--- a/frontend/test/metabase/selectors/metadata.unit.spec.js
+++ b/frontend/test/metabase/selectors/metadata.unit.spec.js
@@ -47,7 +47,7 @@ describe("getMetadata", () => {
     });
 
     it("should have a parent database", () => {
-      expect(table.database).toEqual(metadata.databases[SAMPLE_DATASET.id]);
+      expect(table.database).toEqual(metadata.database(SAMPLE_DATASET.id));
     });
   });
 
-- 
GitLab