diff --git a/.eslintrc b/.eslintrc
index 7ff9ccf95b433cefef37199201f6471afb0d9bda..f522a534d6aba867c232b5e9876a9b7e0583b416 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -2,7 +2,7 @@
     "rules": {
         "strict": [2, "never"],
         "no-undef": 2,
-        "no-unused-vars": [1, {"vars": "all", "args": "none", "varsIgnorePattern": "React|PropTypes|Component"}],
+        "no-unused-vars": [1, {"vars": "all", "args": "none", "varsIgnorePattern": "^(React|PropTypes|Component)$"}],
         "import/no-commonjs": 1,
         "quotes": 0,
         "camelcase": 0,
diff --git a/README.md b/README.md
index 6c3dff4579d816e6ca3c12071988c2bd5f01c904..98437a7abbf4fa5609eaa0552d874e3acbd85d7e 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,7 @@ For more information check out [metabase.com](http://www.metabase.com)
 - CrateDB
 - Oracle
 - Vertica
+- Presto
 
 Don't see your favorite database? File an issue to let us know.
 
diff --git a/bin/ci b/bin/ci
index 461fdffa284967ba745f08edadf10ba88f525c38..4869c1cd4ae31e520fe065a3278870a386de2183 100755
--- a/bin/ci
+++ b/bin/ci
@@ -19,11 +19,15 @@ node-1() {
         run_step lein-test
 }
 node-2() {
-    is_enabled "drivers" && export ENGINES="h2,postgres,sqlite" || export ENGINES="h2"
+    is_enabled "drivers" && export ENGINES="h2,postgres,sqlite,presto" || export ENGINES="h2"
     if is_engine_enabled "crate"; then
         run_step install-crate
     fi
+    if is_engine_enabled "presto"; then
+        run_step install-presto
+    fi
     MB_ENCRYPTION_SECRET_KEY='Orw0AAyzkO/kPTLJRxiyKoBHXa/d6ZcO+p+gpZO/wSQ=' MB_DB_TYPE=mysql MB_DB_DBNAME=circle_test MB_DB_PORT=3306 MB_DB_USER=ubuntu MB_DB_HOST=localhost \
+        MB_PRESTO_HOST=localhost MB_PRESTO_PORT=8080 \
         run_step lein-test
 }
 node-3() {
@@ -91,6 +95,11 @@ install-vertica() {
     sleep 60
 }
 
+install-presto() {
+    docker run --detach --publish 8080:8080 wiill/presto-mb-ci
+    sleep 10
+}
+
 lein-test() {
     lein test
 }
diff --git a/docs/operations-guide/start.md b/docs/operations-guide/start.md
index 6440a074a198f78d7ba0826070dfe1f4b06ddf6b..e405a26d087690332e9ea1fb216a4df1467c929a 100644
--- a/docs/operations-guide/start.md
+++ b/docs/operations-guide/start.md
@@ -70,15 +70,33 @@ Step-by-step instructions on how to upgrade Metabase running on Heroku.
 
 # Troubleshooting Common Problems
 
-### Metabase fails to startup
+### Metabase fails to start due to database locks
 
-Sometimes Metabase will fail to complete its startup due to a database lock that was not cleared properly.
+Sometimes Metabase will fail to complete its startup due to a database lock that was not cleared properly. The error message will look something like:
+
+    liquibase.exception.DatabaseException: liquibase.exception.LockException: Could not acquire change log lock.
 
 When this happens, go to a terminal where Metabase is installed and run:
 
     java -jar metabase.jar migrate release-locks
 
-in the command line to manually clear the locks.  Then restart your Metabase instance.
+in the command line to manually clear the locks. Then restart your Metabase instance.
+
+### Metabase fails to start due to OutOfMemoryErrors
+
+On Java 7, Metabase may fail to launch with a message like
+
+    java.lang.OutOfMemoryError: PermGen space
+
+or one like
+
+    Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler
+
+If this happens, setting a few JVM options should fix your issue:
+
+    java -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -XX:MaxPermSize=256m -jar target/uberjar/metabase.jar
+
+Alternatively, you can upgrade to Java 8 instead, which will fix the issue as well.
 
 
 # Configuring the Metabase Application Database
diff --git a/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx b/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx
index 29c31dbd99275f2afc8c53df334af36ec1f78098..eb6788801c35d82b27302f4e64664f894a7ce076 100644
--- a/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx
+++ b/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx
@@ -3,6 +3,8 @@ import { Link } from "react-router";
 
 import ModalContent from "metabase/components/ModalContent.jsx";
 
+import * as Urls from "metabase/lib/urls";
+
 export default class CreatedDatabaseModal extends Component {
     static propTypes = {
         databaseId: PropTypes.number.isRequired,
@@ -22,7 +24,7 @@ export default class CreatedDatabaseModal extends Component {
                         We're analyzing its schema now to make some educated guesses about its
                         metadata. <Link to={"/admin/datamodel/database/"+databaseId}>View this
                         database</Link> in the Data Model section to see what we've found and to
-                        make edits, or <Link to={"/q#?db="+databaseId}>ask a question</Link> about
+                        make edits, or <Link to={Urls.question(null, `?db=${databaseId}`)}>ask a question</Link> about
                         this database.
                     </p>
                 </div>
diff --git a/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx b/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx
index 1406e5abab63cce70c84d6be159a114421cd1228..ea5abefc8c5f745bece440f429f9836f0d7dfd12 100644
--- a/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx
@@ -2,19 +2,13 @@ import React, { Component, PropTypes } from "react";
 
 import GuiQueryEditor from "metabase/query_builder/components/GuiQueryEditor.jsx";
 
-import { serializeCardForUrl } from "metabase/lib/card";
+import * as Urls from "metabase/lib/urls";
 
-import _ from "underscore";
 import cx from "classnames";
 
-export default class PartialQueryBuilder extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {};
-
-        _.bindAll(this, "setQuery");
-    }
+import * as Query from "metabase/lib/query/query";
 
+export default class PartialQueryBuilder extends Component {
     static propTypes = {
         onChange: PropTypes.func.isRequired,
         tableMetadata: PropTypes.object.isRequired,
@@ -34,15 +28,15 @@ export default class PartialQueryBuilder extends Component {
         });
     }
 
-    setQuery(query) {
-        this.props.onChange(query.query);
-        this.props.updatePreviewSummary(query);
+    setDatasetQuery = (datasetQuery) => {
+        this.props.onChange(datasetQuery.query);
+        this.props.updatePreviewSummary(datasetQuery);
     }
 
     render() {
         let { features, value, tableMetadata, previewSummary } = this.props;
 
-        let dataset_query = {
+        let datasetQuery = {
             type: "query",
             database: tableMetadata.db_id,
             query: {
@@ -53,28 +47,39 @@ export default class PartialQueryBuilder extends Component {
 
         let previewCard = {
             dataset_query: {
-                ...dataset_query,
+                ...datasetQuery,
                 query: {
                     aggregation: ["rows"],
                     breakout: [],
                     filter: [],
-                    ...dataset_query.query
+                    ...datasetQuery.query
                 }
             }
         };
-        let previewUrl = "/q#" + serializeCardForUrl(previewCard);
+        let previewUrl = Urls.question(null, previewCard);
+
+        const onChange = (query) => {
+            this.props.onChange(query);
+            this.props.updatePreviewSummary({ ...datasetQuery, query });
+        }
 
         return (
             <div className="py1">
                 <GuiQueryEditor
-                    query={dataset_query}
                     features={features}
+                    datasetQuery={datasetQuery}
                     tableMetadata={tableMetadata}
                     databases={tableMetadata && [tableMetadata.db]}
-                    setQueryFn={this.setQuery}
+                    setDatasetQuery={this.setDatasetQuery}
                     isShowingDataReference={false}
                     setDatabaseFn={null}
                     setSourceTableFn={null}
+                    addQueryFilter={(filter) => onChange(Query.addFilter(datasetQuery.query, filter))}
+                    updateQueryFilter={(index, filter) => onChange(Query.updateFilter(datasetQuery.query, index, filter))}
+                    removeQueryFilter={(index) => onChange(Query.removeFilter(datasetQuery.query, index))}
+                    addQueryAggregation={(aggregation) => onChange(Query.addAggregation(datasetQuery.query, aggregation))}
+                    updateQueryAggregation={(index, aggregation) => onChange(Query.updateAggregation(datasetQuery.query, index, aggregation))}
+                    removeQueryAggregation={(index) => onChange(Query.removeAggregation(datasetQuery.query, index))}
                 >
                     <div className="flex align-center mx2 my2">
                         <span className="text-bold px3">{previewSummary}</span>
diff --git a/frontend/src/metabase/admin/datamodel/containers/MetricForm.jsx b/frontend/src/metabase/admin/datamodel/containers/MetricForm.jsx
index f3ffc2c8e365b2c4d2ee8fc685c3989a19ec1487..5ad0b08922d20b80bc018b316d59c9e614e54d6e 100644
--- a/frontend/src/metabase/admin/datamodel/containers/MetricForm.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/MetricForm.jsx
@@ -42,12 +42,12 @@ import cx from "classnames";
 },
 metricFormSelectors)
 export default class MetricForm extends Component {
-    updatePreviewSummary(query) {
+    updatePreviewSummary(datasetQuery) {
         this.props.updatePreviewSummary({
-            ...query,
+            ...datasetQuery,
             query: {
                 aggregation: ["count"],
-                ...query.query,
+                ...datasetQuery.query,
             }
         })
     }
diff --git a/frontend/src/metabase/admin/datamodel/containers/SegmentForm.jsx b/frontend/src/metabase/admin/datamodel/containers/SegmentForm.jsx
index f6b10dc6fdd91a127eb314e220f77c417750ab3c..cd99d32ff2475fa2f0fc75353fed4ee827892ea7 100644
--- a/frontend/src/metabase/admin/datamodel/containers/SegmentForm.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/SegmentForm.jsx
@@ -40,11 +40,11 @@ import cx from "classnames";
 },
 segmentFormSelectors)
 export default class SegmentForm extends Component {
-    updatePreviewSummary(query) {
+    updatePreviewSummary(datasetQuery) {
         this.props.updatePreviewSummary({
-            ...query,
+            ...datasetQuery,
             query: {
-                ...query.query,
+                ...datasetQuery.query,
                 aggregation: ["count"]
             }
         })
diff --git a/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx b/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx
index a1827f2f27765897d54d3485191b0f78d44f3fbf..8f98e9d7c280022551901c287a578eb223cc7501 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx
@@ -3,6 +3,7 @@ import React, { Component, PropTypes } from "react";
 import SettingHeader from "./SettingHeader.jsx";
 
 import SettingInput from "./widgets/SettingInput.jsx";
+import SettingNumber from "./widgets/SettingNumber.jsx";
 import SettingPassword from "./widgets/SettingPassword.jsx";
 import SettingRadio from "./widgets/SettingRadio.jsx";
 import SettingToggle from "./widgets/SettingToggle.jsx";
@@ -10,6 +11,7 @@ import SettingSelect from "./widgets/SettingSelect.jsx";
 
 const SETTING_WIDGET_MAP = {
     "string":   SettingInput,
+    "number":   SettingNumber,
     "password": SettingPassword,
     "select":   SettingSelect,
     "radio":    SettingRadio,
diff --git a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx
index 109b043fa9f1bfe3e9a8295e6c5472340a6a0366..c2719ee5c9d4c6434c437efc1c6ff541036bc904 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx
@@ -9,7 +9,7 @@ import Confirm from "metabase/components/Confirm";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
 
 import { CardApi, DashboardApi } from "metabase/services";
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
 
@@ -161,7 +161,7 @@ export const PublicLinksQuestionListing = () =>
         load={CardApi.listPublic}
         revoke={CardApi.deletePublicLink}
         type='Public Card Listing'
-        getUrl={({ id }) => Urls.card(id)}
+        getUrl={({ id }) => Urls.question(id)}
         getPublicUrl={({ public_uuid }) => window.location.origin + Urls.publicCard(public_uuid)}
         noLinksMessage="No questions have been publicly shared yet."
     />;
@@ -177,7 +177,7 @@ export const EmbeddedDashboardListing = () =>
 export const EmbeddedQuestionListing = () =>
     <PublicLinksListing
         load={CardApi.listEmbeddable}
-        getUrl={({ id }) => Urls.card(id)}
+        getUrl={({ id }) => Urls.question(id)}
         type='Embedded Card Listing'
         noLinksMessage="No questions have been embedded yet."
     />;
diff --git a/frontend/src/metabase/admin/settings/components/widgets/SettingNumber.jsx b/frontend/src/metabase/admin/settings/components/widgets/SettingNumber.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..05fd4da0445808f4c7febc128deb46da6df01bd0
--- /dev/null
+++ b/frontend/src/metabase/admin/settings/components/widgets/SettingNumber.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+
+import SettingInput from "./SettingInput";
+
+const SettingNumber = ({ type = "number", ...props }) =>
+    <SettingInput {...props} type="number" />
+
+export default SettingNumber;
diff --git a/frontend/src/metabase/admin/settings/selectors.js b/frontend/src/metabase/admin/settings/selectors.js
index 1f620e2ba88a90be895262ebfe397f88c5861871..ae501e697ee409d3393db53d75673754fa40a973 100644
--- a/frontend/src/metabase/admin/settings/selectors.js
+++ b/frontend/src/metabase/admin/settings/selectors.js
@@ -86,7 +86,7 @@ const SECTIONS = [
                 key: "email-smtp-port",
                 display_name: "SMTP Port",
                 placeholder: "587",
-                type: "string",
+                type: "number",
                 required: true,
                 validations: [["integer", "That's not a valid port number"]]
             },
@@ -236,6 +236,34 @@ const SECTIONS = [
                 getHidden: (settings) => !settings["enable-embedding"]
             }
         ]
+    },
+    {
+        name: "Caching",
+        settings: [
+            {
+                key: "enable-query-caching",
+                display_name: "Enable Caching",
+                type: "boolean"
+            },
+            {
+                key: "query-caching-min-ttl",
+                display_name: "Minimum Query Duration",
+                type: "number",
+                getHidden: (settings) => !settings["enable-query-caching"]
+            },
+            {
+                key: "query-caching-ttl-ratio",
+                display_name: "Cache Time-To-Live (TTL)",
+                type: "number",
+                getHidden: (settings) => !settings["enable-query-caching"]
+            },
+            {
+                key: "query-caching-max-kb",
+                display_name: "Max Cache Entry Size",
+                type: "number",
+                getHidden: (settings) => !settings["enable-query-caching"]
+            }
+        ]
     }
 ];
 for (const section of SECTIONS) {
diff --git a/frontend/src/metabase/components/ActionButton.jsx b/frontend/src/metabase/components/ActionButton.jsx
index fd83b2b465d0ec4da87dc278a707d018ced8ea7c..465fd693c6b50dca5656caac3899d8587748f731 100644
--- a/frontend/src/metabase/components/ActionButton.jsx
+++ b/frontend/src/metabase/components/ActionButton.jsx
@@ -17,6 +17,7 @@ type Props = {
     activeText?: string,
     failedText?: string,
     successText?: string,
+    forceActiveStyle?: boolean
 }
 
 type State = {
@@ -48,7 +49,8 @@ export default class ActionButton extends Component<*, Props, State> {
         normalText: "Save",
         activeText: "Saving...",
         failedText: "Save failed",
-        successText: "Saved"
+        successText: "Saved",
+        forceActiveStyle: false
     };
 
     componentWillUnmount() {
@@ -96,13 +98,13 @@ export default class ActionButton extends Component<*, Props, State> {
 
     render() {
         // eslint-disable-next-line no-unused-vars
-        const { normalText, activeText, failedText, successText, actionFn, className, children, ...props } = this.props;
+        const { normalText, activeText, failedText, successText, actionFn, className, forceActiveStyle, children, ...props } = this.props;
         const { active, result } = this.state;
 
         return (
             <Button
                 {...props}
-                className={cx(className, {
+                    className={forceActiveStyle ? cx('Button', 'Button--waiting') : cx(className, {
                     'Button--waiting pointer-events-none': active,
                     'Button--success': result === 'success',
                     'Button--danger': result === 'failed'
@@ -114,7 +116,7 @@ export default class ActionButton extends Component<*, Props, State> {
                     activeText
                 : result === "success" ?
                     <span>
-                        <Icon name='check' size={12} />
+                        {forceActiveStyle ? null : <Icon name='check' size={12} /> }
                         <span className="ml1">{successText}</span>
                     </span>
                 : result === "failed" ?
diff --git a/frontend/src/metabase/components/AddToDashSelectDashModal.jsx b/frontend/src/metabase/components/AddToDashSelectDashModal.jsx
index 408cb5777523ee39bdb95e3a7bce27f553055a8e..0492804ed33e38179f64bd9d66b10f5a31fceb19 100644
--- a/frontend/src/metabase/components/AddToDashSelectDashModal.jsx
+++ b/frontend/src/metabase/components/AddToDashSelectDashModal.jsx
@@ -5,7 +5,7 @@ import Icon from 'metabase/components/Icon.jsx';
 import ModalContent from "metabase/components/ModalContent.jsx";
 import SortableItemList from 'metabase/components/SortableItemList.jsx';
 
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 import { DashboardApi } from "metabase/services";
 
 import moment from 'moment';
diff --git a/frontend/src/metabase/components/DownloadButton.jsx b/frontend/src/metabase/components/DownloadButton.jsx
index 003f86365b2dfb90d8328cd7ea40078ee118f94a..2095f254563529a1ecf8d76f3a6b300885a7edc1 100644
--- a/frontend/src/metabase/components/DownloadButton.jsx
+++ b/frontend/src/metabase/components/DownloadButton.jsx
@@ -1,5 +1,4 @@
 import React, { Component, PropTypes } from "react";
-import ReactDOM from "react-dom";
 
 import Button from "metabase/components/Button.jsx";
 
diff --git a/frontend/src/metabase/components/NotFound.jsx b/frontend/src/metabase/components/NotFound.jsx
index 0eb16a19945a2dbe89ac36d3a778b913b0820d96..cdf98c4afcac3c1fccdfc438d34ca0e947f6344b 100644
--- a/frontend/src/metabase/components/NotFound.jsx
+++ b/frontend/src/metabase/components/NotFound.jsx
@@ -1,6 +1,8 @@
 import React, { Component, PropTypes } from "react";
 import { Link } from "react-router";
 
+import * as Urls from "metabase/lib/urls";
+
 export default class NotFound extends Component {
     render() {
         return (
@@ -11,7 +13,7 @@ export default class NotFound extends Component {
                     <p className="h4">You might've been tricked by a ninja, but in all likelihood, you were just given a bad link.</p>
                     <p className="h4 my4">You can always:</p>
                     <div className="flex align-center">
-                        <Link to="/q" className="Button Button--primary">
+                        <Link to={Urls.question()} className="Button Button--primary">
                             <div className="p1">Ask a new question.</div>
                         </Link>
                         <span className="mx2">or</span>
diff --git a/frontend/src/metabase/components/Select.jsx b/frontend/src/metabase/components/Select.jsx
index 5fb23f9a3465f07cca051609c0ce5a0b735016b4..20adc9c358a24b0216b040e7acf630be9878d727 100644
--- a/frontend/src/metabase/components/Select.jsx
+++ b/frontend/src/metabase/components/Select.jsx
@@ -79,13 +79,8 @@ class BrowserSelect extends Component {
             <PopoverWithTrigger
                 ref="popover"
                 className={className}
-                triggerElement={
-                    <div className={"flex align-center " + (!value ? " text-grey-3" : "")}>
-                        <span className="AdminSelect-content mr1">{selectedName}</span>
-                        <Icon className="AdminSelect-chevron flex-align-right" name="chevrondown" size={12} />
-                    </div>
-                }
-                triggerClasses={cx("AdminSelect", className)}
+                triggerElement={<SelectButton hasValue={!!value}>{selectedName}</SelectButton>}
+                triggerClasses={className}
                 verticalAttachments={["top"]}
                 isInitiallyOpen={isInitiallyOpen}
             >
@@ -117,6 +112,17 @@ class BrowserSelect extends Component {
     }
 }
 
+export const SelectButton = ({ hasValue, children }) =>
+    <div className={"AdminSelect flex align-center " + (!hasValue ? " text-grey-3" : "")}>
+        <span className="AdminSelect-content mr1">{children}</span>
+        <Icon className="AdminSelect-chevron flex-align-right" name="chevrondown" size={12} />
+    </div>
+
+SelectButton.propTypes = {
+    hasValue: PropTypes.bool,
+    children: PropTypes.any,
+};
+
 export class Option extends Component {
     static propTypes = {
         children:   PropTypes.any,
diff --git a/frontend/src/metabase/components/ShrinkableList.jsx b/frontend/src/metabase/components/ShrinkableList.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..15f9a89a19fdd06548923f97eda749b3be7c95f9
--- /dev/null
+++ b/frontend/src/metabase/components/ShrinkableList.jsx
@@ -0,0 +1,59 @@
+/* @flow */
+
+import React, { Component, PropTypes } from "react";
+import ReactDOM from "react-dom";
+
+import ExplicitSize from "metabase/components/ExplicitSize";
+
+type Props = {
+    className?: string,
+    items: any[],
+    renderItem: (item: any) => any,
+    renderItemSmall: (item: any) => any
+};
+
+type State = {
+    isShrunk: ?boolean
+};
+
+@ExplicitSize
+export default class ShrinkableList extends Component<*, Props, State> {
+    state: State = {
+        isShrunk: null
+    }
+
+    componentWillReceiveProps() {
+        this.setState({
+            isShrunk: null
+        })
+    }
+
+    componentDidMount() {
+        this.componentDidUpdate();
+    }
+
+    componentDidUpdate() {
+        const container = ReactDOM.findDOMNode(this)
+        const { isShrunk } = this.state;
+        if (container && isShrunk === null) {
+            this.setState({
+                isShrunk: container.scrollWidth !== container.offsetWidth
+            })
+        }
+    }
+
+    render() {
+        const { items, className, renderItemSmall, renderItem } = this.props;
+        const { isShrunk } = this.state;
+        return (
+            <div className={className}>
+                { items.map(item =>
+                    isShrunk ?
+                        renderItemSmall(item)
+                    :
+                        renderItem(item)
+                )}
+            </div>
+        );
+    }
+}
diff --git a/frontend/src/metabase/components/Tooltip.jsx b/frontend/src/metabase/components/Tooltip.jsx
index 68a84a2c6a3343882c046e3ee9bfab27ffbc4395..b892ba3e0fdfc840742c4de580e78be3b9a9350f 100644
--- a/frontend/src/metabase/components/Tooltip.jsx
+++ b/frontend/src/metabase/components/Tooltip.jsx
@@ -14,7 +14,7 @@ export default class Tooltip extends Component {
     }
 
     static propTypes = {
-        tooltip: PropTypes.node.isRequired,
+        tooltip: PropTypes.node,
         children: PropTypes.element.isRequired,
         isEnabled: PropTypes.bool,
         verticalAttachments: PropTypes.array,
diff --git a/frontend/src/metabase/css/core/flex.css b/frontend/src/metabase/css/core/flex.css
index 9ad2d5853c9fea87147af5128d3264e0e8de72e1..f88fcf8efb97e6ba990655bf4b5468b492994915 100644
--- a/frontend/src/metabase/css/core/flex.css
+++ b/frontend/src/metabase/css/core/flex.css
@@ -32,6 +32,10 @@
     justify-content: space-between;
 }
 
+.justify-end {
+    justify-content: flex-end;
+}
+
 .align-start {
     align-items: flex-start;
 }
diff --git a/frontend/src/metabase/css/query_builder.css b/frontend/src/metabase/css/query_builder.css
index a8cb318deca637f3a5104b37fdbd098911dc7753..f298eff4caf824a7d90164dc5ec9cfb288b2c88e 100644
--- a/frontend/src/metabase/css/query_builder.css
+++ b/frontend/src/metabase/css/query_builder.css
@@ -518,12 +518,13 @@
     z-index: 1;
     opacity: 1;
     box-shadow: 0 1px 2px rgba(0, 0, 0, .22);
-    transition: margin-top 0.5s, opacity 0.5s;
+    transition: transform 0.5s, opacity 0.5s;
     min-width: 8em;
+    position: relative;
 }
 
 .RunButton.RunButton--hidden {
-    margin-top: -110px;
+    transform: translateY(-65px);
     opacity: 0;
 }
 
diff --git a/frontend/src/metabase/dashboard/components/Dashboard.jsx b/frontend/src/metabase/dashboard/components/Dashboard.jsx
index 6ff8eddaf882ca5377d7bd913ae43ade6c202c01..931f3d9969ea0b7ef2366c8923f40bc3c2876481 100644
--- a/frontend/src/metabase/dashboard/components/Dashboard.jsx
+++ b/frontend/src/metabase/dashboard/components/Dashboard.jsx
@@ -1,5 +1,4 @@
 import React, { Component, PropTypes } from "react";
-import ReactDOM from "react-dom";
 
 import DashboardHeader from "../components/DashboardHeader.jsx";
 import DashboardGrid from "../components/DashboardGrid.jsx";
diff --git a/frontend/src/metabase/dashboard/components/DashboardGrid.jsx b/frontend/src/metabase/dashboard/components/DashboardGrid.jsx
index a8fda47120ea8019c567dd8b2b05bfeda9dabb89..a037198fc1b42e9a037b36d1dabe345f93f1cebe 100644
--- a/frontend/src/metabase/dashboard/components/DashboardGrid.jsx
+++ b/frontend/src/metabase/dashboard/components/DashboardGrid.jsx
@@ -1,5 +1,4 @@
 import React, { Component, PropTypes } from "react";
-import ReactDOM from "react-dom";
 
 import GridLayout from "./grid/GridLayout.jsx";
 import DashCard from "./DashCard.jsx";
diff --git a/frontend/src/metabase/dashboard/components/parameters/ParameterValueWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/ParameterValueWidget.jsx
index 4dc174858e4f88922e824912454125baaa5a0b83..4aacaf8dff48740d3752a72e4df363ff07088f3f 100644
--- a/frontend/src/metabase/dashboard/components/parameters/ParameterValueWidget.jsx
+++ b/frontend/src/metabase/dashboard/components/parameters/ParameterValueWidget.jsx
@@ -9,6 +9,7 @@ import DateRangeWidget from "./widgets/DateRangeWidget.jsx";
 import DateRelativeWidget from "./widgets/DateRelativeWidget.jsx";
 import DateMonthYearWidget from "./widgets/DateMonthYearWidget.jsx";
 import DateQuarterYearWidget from "./widgets/DateQuarterYearWidget.jsx";
+import DateAllOptionsWidget from "./widgets/DateAllOptionsWidget.jsx";
 import CategoryWidget from "./widgets/CategoryWidget.jsx";
 import TextWidget from "./widgets/TextWidget.jsx";
 
@@ -22,6 +23,7 @@ const WIDGETS = {
     "date/relative": DateRelativeWidget,
     "date/month-year": DateMonthYearWidget,
     "date/quarter-year": DateQuarterYearWidget,
+    "date/all-options": DateAllOptionsWidget
 }
 
 export default class ParameterValueWidget extends Component {
diff --git a/frontend/src/metabase/dashboard/components/parameters/ParametersPopover.jsx b/frontend/src/metabase/dashboard/components/parameters/ParametersPopover.jsx
index ff2e3734277debf58b5abd0a87050b98e17461ec..d1788e09a6685d0dd315d0ecf388d9198a4db1db 100644
--- a/frontend/src/metabase/dashboard/components/parameters/ParametersPopover.jsx
+++ b/frontend/src/metabase/dashboard/components/parameters/ParametersPopover.jsx
@@ -58,7 +58,7 @@ const ParameterOptionsSectionsPane = ({ sections, onSelectSection }) =>
 
 const ParameterOptionItem = ({ option, onClick }) =>
     <li onClick={onClick} className="p1 px2 cursor-pointer brand-hover">
-        <div className="text-brand text-bold">{option.name}</div>
+        <div className="text-brand text-bold">{option.menuName || option.name}</div>
         <div>{option.description}</div>
     </li>
 
diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateAllOptionsWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/DateAllOptionsWidget.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..8c6bf8188c4d73492833bf63dbbc0eb6db3a8189
--- /dev/null
+++ b/frontend/src/metabase/dashboard/components/parameters/widgets/DateAllOptionsWidget.jsx
@@ -0,0 +1,148 @@
+/* @flow */
+
+import React, {Component, PropTypes} from "react";
+import cx from "classnames";
+
+import DatePicker, {DATE_OPERATORS} from "metabase/query_builder/components/filters/pickers/DatePicker.jsx";
+import {generateTimeFilterValuesDescriptions} from "metabase/lib/query_time";
+
+import type {OperatorName} from "metabase/query_builder/components/filters/pickers/DatePicker.jsx";
+import type {FieldFilter, LocalFieldReference} from "metabase/meta/types/Query";
+
+type UrlEncoded = string;
+// $FlowFixMe
+type RegexMatches = [string];
+type Deserializer = (RegexMatches) => FieldFilter;
+
+// Use a placeholder value as field references are not used in dashboard filters
+// $FlowFixMe
+const noopRef: LocalFieldReference = null;
+
+function getFilterValueSerializer(func: ((val1: string, val2: string) => UrlEncoded)) {
+    // $FlowFixMe
+    return filter => func(filter[2], filter[3])
+}
+
+const serializersByOperatorName: { [id: OperatorName]: (FieldFilter) => UrlEncoded } = {
+    // $FlowFixMe
+    "Previous": getFilterValueSerializer((value, unit) => `past${-value}${unit}s`),
+    "Next": getFilterValueSerializer((value, unit) => `next${value}${unit}s`),
+    "Current": getFilterValueSerializer((_, unit) => `this${unit}`),
+    "Before": getFilterValueSerializer((value) => `~${value}`),
+    "After": getFilterValueSerializer((value) => `${value}~`),
+    "On": getFilterValueSerializer((value) => `${value}`),
+    "Between": getFilterValueSerializer((from, to) => `${from}~${to}`)
+};
+
+function getFilterOperator(filter) {
+    return DATE_OPERATORS.find((op) => op.test(filter));
+}
+function filterToUrlEncoded(filter: FieldFilter): ?UrlEncoded {
+    const operator = getFilterOperator(filter)
+
+    if (operator) {
+        return serializersByOperatorName[operator.name](filter);
+    } else {
+        return null;
+    }
+}
+
+const deserializersWithTestRegex: [{ testRegex: RegExp, deserialize: Deserializer}] = [
+    {testRegex: /^past([0-9]+)([a-z]+)s$/, deserialize: (matches) => {
+        return ["time-interval", noopRef, -parseInt(matches[0]), matches[1]]
+    }},
+    {testRegex: /^next([0-9]+)([a-z]+)s$/, deserialize: (matches) => {
+        return ["time-interval", noopRef, parseInt(matches[0]), matches[1]]
+    }},
+    {testRegex: /^this([a-z]+)$/, deserialize: (matches) => ["time-interval", noopRef, "current", matches[0]] },
+    {testRegex: /^~([0-9-T:]+)$/, deserialize: (matches) => ["<", noopRef, matches[0]]},
+    {testRegex: /^([0-9-T:]+)~$/, deserialize: (matches) => [">", noopRef, matches[0]]},
+    {testRegex: /^([0-9-T:]+)$/, deserialize: (matches) => ["=", noopRef, matches[0]]},
+    // TODO 3/27/17 Atte Keinänen
+    // Unify BETWEEN -> between, IS_NULL -> is-null, NOT_NULL -> not-null throughout the codebase
+    // $FlowFixMe
+    {testRegex: /^([0-9-T:]+)~([0-9-T:]+)$/, deserialize: (matches) => ["BETWEEN", noopRef, matches[0], matches[1]]},
+];
+
+function urlEncodedToFilter(urlEncoded: UrlEncoded): ?FieldFilter {
+    const deserializer =
+        deserializersWithTestRegex.find((des) => urlEncoded.search(des.testRegex) !== -1);
+
+    if (deserializer) {
+        const substringMatches = deserializer.testRegex.exec(urlEncoded).splice(1);
+        return deserializer.deserialize(substringMatches);
+    } else {
+        return null;
+    }
+}
+
+const prefixedOperators: [OperatorName] = ["Before", "After", "On", "Is Empty", "Not Empty"];
+function getFilterTitle(filter) {
+    const desc = generateTimeFilterValuesDescriptions(filter).join(" - ")
+    const op = getFilterOperator(filter);
+    const prefix = op && prefixedOperators.indexOf(op.name) !== -1 ? `${op.name} ` : "";
+    return prefix + desc;
+}
+
+type Props = {
+    setValue: (value: ?string) => void,
+    onClose: () => void
+};
+
+type State = { filter: FieldFilter };
+
+export default class DateAllOptionsWidget extends Component<*, Props, State> {
+    state: State;
+
+    constructor(props: Props) {
+        super(props);
+
+        this.state = {
+            // $FlowFixMe
+            filter: props.value != null ? urlEncodedToFilter(props.value) || [] : []
+        }
+    }
+
+    static propTypes = {};
+    static defaultProps = {};
+
+    static format = (urlEncoded: ?string) => {
+        if (urlEncoded == null) return null;
+        const filter = urlEncodedToFilter(urlEncoded);
+
+        return filter ? getFilterTitle(filter) : null;
+    };
+
+    commitAndClose = () => {
+        this.props.setValue(filterToUrlEncoded(this.state.filter));
+        this.props.onClose()
+    }
+
+    setFilter = (filter: FieldFilter) => {
+        this.setState({filter});
+    }
+
+    isValid() {
+        const filterValues = this.state.filter.slice(2);
+        return filterValues.every((value) => value != null);
+    }
+
+    render() {
+        return (<div style={{minWidth: "300px"}}>
+            <DatePicker
+                filter={this.state.filter}
+                onFilterChange={this.setFilter}
+                hideEmptinessOperators
+                hideTimeSelectors
+            />
+            <div className="FilterPopover-footer p1">
+                <button
+                    className={cx("Button Button--purple full", {"disabled": !this.isValid()})}
+                    onClick={this.commitAndClose}
+                >
+                    Update filter
+                </button>
+            </div>
+        </div>)
+    }
+}
diff --git a/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx b/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx
index 6ff0deeb50520db60ecf107be57925c318123142..50ca40dee81a890a314713e9f0adac57686d7c9a 100644
--- a/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx
+++ b/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx
@@ -5,7 +5,7 @@ import { connect } from "react-redux";
 
 import EmbedWidget from "metabase/public/components/widgets/EmbedWidget";
 
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import { createPublicLink, deletePublicLink, updateEnableEmbedding, updateEmbeddingParams } from "../dashboard";
 
diff --git a/frontend/src/metabase/dashboard/containers/ParameterWidget.css b/frontend/src/metabase/dashboard/containers/ParameterWidget.css
index fd52f835c96d35d0f5cd943b22a8fbc9180792b0..d7704049805f47a1c9f82cc979a71d665dbdd6fe 100644
--- a/frontend/src/metabase/dashboard/containers/ParameterWidget.css
+++ b/frontend/src/metabase/dashboard/containers/ParameterWidget.css
@@ -12,7 +12,8 @@
     position: relative;
     height: 0;
     line-height: 0;
-    margin-left: 0.5em;
+    margin-left: -0.45em;
+    padding: 0 0.5em;
 }
 
 :local(.container.deemphasized) {
diff --git a/frontend/src/metabase/home/components/Activity.jsx b/frontend/src/metabase/home/components/Activity.jsx
index 69dcce269099d519495e83ac6a2ff91787aca92f..bc8d65ca2c60aab05fb1102ef878b4661f4c9731 100644
--- a/frontend/src/metabase/home/components/Activity.jsx
+++ b/frontend/src/metabase/home/components/Activity.jsx
@@ -6,7 +6,7 @@ import LoadingAndErrorWrapper from 'metabase/components/LoadingAndErrorWrapper.j
 import ActivityItem from './ActivityItem.jsx';
 import ActivityStory from './ActivityStory.jsx';
 
-import Urls from 'metabase/lib/urls';
+import * as Urls from "metabase/lib/urls";
 
 export default class Activity extends Component {
 
@@ -213,7 +213,7 @@ export default class Activity extends Component {
             case "dashboard-remove-cards":
                 description.body = item.details.dashcards[0].name;
                 if (item.details.dashcards[0].exists) {
-                    description.bodyLink = Urls.card(item.details.dashcards[0].card_id);
+                    description.bodyLink = Urls.question(item.details.dashcards[0].card_id);
                 }
                 break;
             case "metric-create":
diff --git a/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx b/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx
index 1de3e68b33880d05c0de16500768e7c36579329b..d58b1ff6912110f687a4df04804bae7e4fc7f8a5 100644
--- a/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx
+++ b/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx
@@ -2,6 +2,7 @@ import React, { Component, PropTypes } from "react";
 import { Link } from "react-router";
 
 import MetabaseSettings from "metabase/lib/settings";
+import * as Urls from "metabase/lib/urls";
 
 export default class NewUserOnboardingModal extends Component {
     constructor(props, context) {
@@ -84,7 +85,7 @@ export default class NewUserOnboardingModal extends Component {
                             {this.renderStep()}
                             <span className="flex-align-right">
                                 <a className="text-underline-hover cursor-pointer mr3" onClick={() => (this.closeModal())}>skip for now</a>
-                                <Link to="/q#?tutorial" className="Button Button--primary">Let's do it!</Link>
+                                <Link to={Urls.question(null, "?tutorial")} className="Button Button--primary">Let's do it!</Link>
                             </span>
                         </div>
                     </div>
diff --git a/frontend/src/metabase/home/components/RecentViews.jsx b/frontend/src/metabase/home/components/RecentViews.jsx
index f1146966a807b999db69613662d91df7f7d40d4f..9a967584956eed30780e4128ede71e705e7f7b1e 100644
--- a/frontend/src/metabase/home/components/RecentViews.jsx
+++ b/frontend/src/metabase/home/components/RecentViews.jsx
@@ -3,7 +3,7 @@ import { Link } from "react-router";
 
 import Icon from "metabase/components/Icon.jsx";
 import SidebarSection from "./SidebarSection.jsx";
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import { normal } from "metabase/lib/colors";
 
diff --git a/frontend/src/metabase/icon_paths.js b/frontend/src/metabase/icon_paths.js
index e1c4bf1a7780d6e9b84d4ca8870fb3212c3b4675..a4c0902142b699967314a7f7bd6e42040f8f51b0 100644
--- a/frontend/src/metabase/icon_paths.js
+++ b/frontend/src/metabase/icon_paths.js
@@ -42,6 +42,9 @@ export var ICON_PATHS = {
         path: 'M19.4375,6.97396734 L27,8.34417492 L27,25.5316749 L18.7837192,23.3765689 L11.875,25.3366259 L11.875,25.3366259 L11.875,11.0941749 L11.1875,11.0941749 L11.1875,25.5316749 L5,24.1566749 L5,7.65667492 L11.1875,9.03167492 L18.75,6.90135976 L18.75,22.0941749 L19.4375,22.0941749 L19.4375,6.97396734 Z',
         attrs: { scale: 2 }
     },
+    compass_needle: {
+        path: 'M0 32l10.706-21.064L32 0 21.22 20.89 0 32zm16.092-12.945a3.013 3.013 0 0 0 3.017-3.009 3.013 3.013 0 0 0-3.017-3.008 3.013 3.013 0 0 0-3.017 3.008 3.013 3.013 0 0 0 3.017 3.009z'
+    },
     connections: {
         path: 'M5.37815706,11.5570815 C5.55061975,11.1918363 5.64705882,10.783651 5.64705882,10.3529412 C5.64705882,9.93118218 5.55458641,9.53102128 5.38881053,9.1716274 L11.1846365,4.82475792 C11.6952189,5.33295842 12.3991637,5.64705882 13.1764706,5.64705882 C14.7358628,5.64705882 16,4.38292165 16,2.82352941 C16,1.26413718 14.7358628,0 13.1764706,0 C11.6170784,0 10.3529412,1.26413718 10.3529412,2.82352941 C10.3529412,3.2452884 10.4454136,3.64544931 10.6111895,4.00484319 L10.6111895,4.00484319 L4.81536351,8.35171266 C4.3047811,7.84351217 3.60083629,7.52941176 2.82352941,7.52941176 C1.26413718,7.52941176 0,8.79354894 0,10.3529412 C0,11.9123334 1.26413718,13.1764706 2.82352941,13.1764706 C3.59147157,13.1764706 4.28780867,12.8698929 4.79682555,12.3724528 L10.510616,16.0085013 C10.408473,16.3004758 10.3529412,16.6143411 10.3529412,16.9411765 C10.3529412,18.5005687 11.6170784,19.7647059 13.1764706,19.7647059 C14.7358628,19.7647059 16,18.5005687 16,16.9411765 C16,15.3817842 14.7358628,14.1176471 13.1764706,14.1176471 C12.3029783,14.1176471 11.5221273,14.5142917 11.0042049,15.1372938 L5.37815706,11.5570815 Z',
         attrs: { viewBox: '0 0 16 19.7647' }
diff --git a/frontend/src/metabase/lib/card.js b/frontend/src/metabase/lib/card.js
index cbbebbd7b55a5e27a31bbc16ad909f0e0fa99e88..2b0ac9090b755dc86b2a981f6fdd1cb00de4faa3 100644
--- a/frontend/src/metabase/lib/card.js
+++ b/frontend/src/metabase/lib/card.js
@@ -1,6 +1,7 @@
 import _ from "underscore";
 import Query, { createQuery } from "metabase/lib/query";
 import Utils from "metabase/lib/utils";
+import * as Urls from "metabase/lib/urls";
 
 import { CardApi } from "metabase/services";
 
@@ -24,10 +25,10 @@ export function startNewCard(type, databaseId, tableId) {
 }
 
 // load a card either by ID or from a base64 serialization.  if both are present then they are merged, which the serialized version taking precedence
+// TODO: move to redux
 export async function loadCard(cardId) {
     try {
-        let card = await CardApi.get({ "cardId": cardId });
-        return card && cleanCopyCard(card);
+        return await CardApi.get({ "cardId": cardId });
     } catch (error) {
         console.log("error loading card", error);
         throw error;
@@ -64,11 +65,11 @@ export function isCardRunnable(card, tableMetadata) {
     if (!card) {
         return false;
     }
-    const query = card.dataset_query;
-    if (query.query) {
-        return Query.canRun(query.query, tableMetadata);
+    const datasetQuery = card.dataset_query;
+    if (datasetQuery.query) {
+        return Query.canRun(datasetQuery.query, tableMetadata);
     } else {
-        return (query.database != undefined && query.native.query !== "");
+        return (datasetQuery.database != undefined && datasetQuery.native.query !== "");
     }
 }
 
@@ -111,16 +112,10 @@ export function b64url_to_utf8(b64url) {
 }
 
 export function urlForCardState(state, dirty) {
-    var url;
-    if (state.cardId) {
-        url = "/card/" + state.cardId;
-    } else {
-        url = "/q";
-    }
-    if (state.serializedCard && dirty) {
-        url += "#" + state.serializedCard;
-    }
-    return url;
+    return Urls.question(
+        state.cardId,
+        (state.serializedCard && dirty) ? state.serializedCard : ""
+    );
 }
 
 export function cleanCopyCard(card) {
diff --git a/frontend/src/metabase/lib/data_grid.js b/frontend/src/metabase/lib/data_grid.js
index e4a5fb4c2fafe398c8276c981bcd5e85c07d5a61..55895e556c7dab165b039eb01faf75280db95a4e 100644
--- a/frontend/src/metabase/lib/data_grid.js
+++ b/frontend/src/metabase/lib/data_grid.js
@@ -37,16 +37,19 @@ export function pivot(data) {
         normalColValues.sort();
     }
 
-
     // make sure that the first element in the pivoted column list is null which makes room for the label of the other column
     pivotColValues.unshift(data.cols[normalCol].display_name);
 
     // start with an empty grid that we'll fill with the appropriate values
-    var pivotedRows = [];
-    var emptyRow = Array.apply(null, Array(pivotColValues.length)).map(function() { return null; });
-    for (var i=0; i < normalColValues.length; i++) {
-        pivotedRows.push(_.clone(emptyRow));
-    }
+    const pivotedRows = normalColValues.map((normalColValues, index) => {
+        const row = pivotColValues.map(() => null);
+        // for onVisualizationClick:
+        row._dimension = {
+            value: normalColValues,
+            column: data.cols[normalCol]
+        };
+        return row;
+    })
 
     // fill it up with the data
     for (var j=0; j < data.rows.length; j++) {
@@ -59,14 +62,20 @@ export function pivot(data) {
     }
 
     // provide some column metadata to maintain consistency
-    var cols = pivotColValues.map(function(val, idx) {
+    const cols = pivotColValues.map(function(value, idx) {
         if (idx === 0) {
             // first column is always the coldef of the normal column
             return data.cols[normalCol];
         }
 
         var colDef = _.clone(data.cols[cellCol]);
-        colDef['name'] = colDef['display_name'] = formatValue(val, { column: data.cols[pivotCol] }) || "";
+        colDef.name = colDef.display_name = formatValue(value, { column: data.cols[pivotCol] }) || "";
+        // for onVisualizationClick:
+        colDef._dimension = {
+            value: value,
+            column: data.cols[pivotCol]
+        };
+        // delete colDef.id
         return colDef;
     });
 
diff --git a/frontend/src/metabase/lib/dataset.js b/frontend/src/metabase/lib/dataset.js
new file mode 100644
index 0000000000000000000000000000000000000000..c5e56bf62088ae03f3cf17a4f883669ad49a9fef
--- /dev/null
+++ b/frontend/src/metabase/lib/dataset.js
@@ -0,0 +1,4 @@
+import _ from "underscore";
+
+// Many aggregations result in [[null]] if there are no rows to aggregate after filters
+export const datasetContainsNoResults = (data) => data.rows.length === 0 || _.isEqual(data.rows, [[null]])
diff --git a/frontend/src/metabase/lib/dom.js b/frontend/src/metabase/lib/dom.js
index 1c1f0c27e922bdfdeb5a20c158d1f90e010f645a..26a11b0385b94faf4066358028102d45da770b62 100644
--- a/frontend/src/metabase/lib/dom.js
+++ b/frontend/src/metabase/lib/dom.js
@@ -178,7 +178,7 @@ var STYLE_SHEET = (function() {
     return style.sheet;
 })();
 
-export function addCSSRule(selector, rules, index) {
+export function addCSSRule(selector, rules, index = 0) {
     if("insertRule" in STYLE_SHEET) {
         STYLE_SHEET.insertRule(selector + "{" + rules + "}", index);
     }
diff --git a/frontend/src/metabase/lib/formatting.js b/frontend/src/metabase/lib/formatting.js
index e80aa5f8abfc576b49e50bcf97aa761381421c9f..1a2a30921af916035253fe3af8f0f1092a0d90f7 100644
--- a/frontend/src/metabase/lib/formatting.js
+++ b/frontend/src/metabase/lib/formatting.js
@@ -64,7 +64,7 @@ function formatMajorMinor(major, minor, options = {}) {
     }
 }
 
-function formatTimeWithUnit(value, unit, options = {}) {
+export function formatTimeWithUnit(value, unit, options = {}) {
     let m = parseTimestamp(value, unit);
     if (!m.isValid()) {
         return String(value);
diff --git a/frontend/src/metabase/lib/query.js b/frontend/src/metabase/lib/query.js
index 9e9dfbddb224929f73d164b7488bed9c475420c7..e8d0360e122e7eb09de383ee364d07adbacd14b5 100644
--- a/frontend/src/metabase/lib/query.js
+++ b/frontend/src/metabase/lib/query.js
@@ -10,6 +10,7 @@ import { isFK, TYPE } from "metabase/lib/types";
 import { stripId } from "metabase/lib/formatting";
 import { format as formatExpression } from "metabase/lib/expressions/formatter";
 
+import * as Table from "./query/table";
 
 import * as Q from "./query/query";
 import { mbql, mbqlEq } from "./query/util";
@@ -270,7 +271,7 @@ var Query = {
     },
 
     getExpressions(query) {
-        return query.expressions;
+        return query.expressions || {};
     },
 
     setExpression(query, name, expression) {
@@ -744,18 +745,6 @@ export const BreakoutClause = {
     }
 }
 
-const Table = {
-    getField(table, fieldId) {
-        if (table) {
-            // sometimes we populate fields_lookup, sometimes we don't :(
-            if (table.fields_lookup) {
-                return table.fields_lookup[fieldId];
-            } else {
-                return _.findWhere(table.fields, { id: fieldId });
-            }
-        }
-    }
-}
 
 function joinList(list, joiner) {
     return _.flatten(list.map((l, i) => i === list.length - 1 ? [l] : [l, joiner]), true);
diff --git a/frontend/src/metabase/lib/query/breakout.js b/frontend/src/metabase/lib/query/breakout.js
index 8e7954e227b37974f0d45e8d077c4553f7f602ed..5e1f0f9b6961fa8cf43ea1f5266572217868e8d2 100644
--- a/frontend/src/metabase/lib/query/breakout.js
+++ b/frontend/src/metabase/lib/query/breakout.js
@@ -1,12 +1,20 @@
 /* @flow */
 
 import type { Breakout, BreakoutClause } from "metabase/meta/types/Query";
+import type { TableMetadata } from "metabase/meta/types/Metadata";
+import type { Field } from "metabase/meta/types/Field";
+
+import Q from "metabase/lib/query";
 
 import { add, update, remove, clear } from "./util";
 
 // returns canonical list of Breakouts, with nulls removed
-export function getBreakouts(breakout: ?BreakoutClause): Breakout[] {
-    return (breakout || []).filter(b => b != null);
+export function getBreakouts(breakouts: ?BreakoutClause): Breakout[] {
+    return (breakouts || []).filter(b => b != null);
+}
+
+export function getBreakoutFields(breakouts: ?BreakoutClause, tableMetadata: TableMetadata): Field[] {
+    return getBreakouts(breakouts).map(breakout => (Q.getFieldTarget(breakout, tableMetadata) || {}).field);
 }
 
 // turns a list of Breakouts into the canonical BreakoutClause
diff --git a/frontend/src/metabase/lib/query/expression.js b/frontend/src/metabase/lib/query/expression.js
new file mode 100644
index 0000000000000000000000000000000000000000..e8612329c618ed9d05e84ee003dfa949edbed196
--- /dev/null
+++ b/frontend/src/metabase/lib/query/expression.js
@@ -0,0 +1,27 @@
+import _ from "underscore";
+
+import type { ExpressionName, ExpressionClause, Expression } from "metabase/meta/types/Query";
+
+export function getExpressions(expressions: ?ExpressionClause = {}): ExpressionClause {
+    return expressions;
+}
+
+export function getExpressionsList(expressions: ?ExpressionClause = {}): Array<{ name: ExpressionName, expression: Expression }> {
+    return Object.entries(expressions).map(([name, expression]) => ({ name, expression }));
+}
+
+export function addExpression(expressions: ?ExpressionClause = {}, name: ExpressionName, expression: Expression): ?ExpressionClause {
+    return { ...expressions, [name]: expression };
+}
+export function updateExpression(expressions: ?ExpressionClause = {}, name: ExpressionName, expression: Expression, oldName?: ExpressionName): ?ExpressionClause {
+    if (oldName != null) {
+        expressions = removeExpression(expressions, oldName);
+    }
+    return addExpression(expressions, name, expression);
+}
+export function removeExpression(expressions: ?ExpressionClause = {}, name: ExpressionName): ?ExpressionClause {
+    return _.omit(expressions, name)
+}
+export function clearExpressions(expressions: ?ExpressionClause): ?ExpressionClause {
+    return {};
+}
diff --git a/frontend/src/metabase/lib/query/filter.js b/frontend/src/metabase/lib/query/filter.js
index c36a1629d064b1074eedf2cf2877c424910a308f..8f3851141ecba3fae9eb97e66791a5ccc4e39c48 100644
--- a/frontend/src/metabase/lib/query/filter.js
+++ b/frontend/src/metabase/lib/query/filter.js
@@ -48,3 +48,15 @@ export function canAddFilter(filter: ?FilterClause): boolean {
     }
     return true;
 }
+
+export function isSegmentFilter(filter: FilterClause): boolean {
+    return Array.isArray(filter) && mbqlEq(filter[0], "segment");
+}
+
+export function isCompoundFilter(filter: FilterClause): boolean {
+    return Array.isArray(filter) && (mbqlEq(filter[0], "and") || mbqlEq(filter[0], "or"));
+}
+
+export function isFieldFilter(filter: FilterClause): boolean {
+    return !isSegmentFilter(filter) && !isCompoundFilter(filter);
+}
diff --git a/frontend/src/metabase/lib/query/query.js b/frontend/src/metabase/lib/query/query.js
index de1973c40c5c55d7fa5196d1eb6495a0b2a692c4..56a7501001eea2fa9f60feffc7ecde7bc489f6e3 100644
--- a/frontend/src/metabase/lib/query/query.js
+++ b/frontend/src/metabase/lib/query/query.js
@@ -6,14 +6,17 @@ import type {
     Breakout, BreakoutClause,
     Filter, FilterClause,
     LimitClause,
-    OrderBy, OrderByClause
+    OrderBy, OrderByClause,
+    ExpressionClause, ExpressionName, Expression
 } from "metabase/meta/types/Query";
+import type { TableMetadata } from "metabase/meta/types/Metadata";
 
 import * as A from "./aggregation";
 import * as B from "./breakout";
 import * as F from "./filter";
 import * as L from "./limit";
 import * as O from "./order_by";
+import * as E from "./expression";
 
 import Query from "metabase/lib/query";
 import _ from "underscore";
@@ -38,6 +41,8 @@ export const updateBreakout = (query: SQ, index: number, breakout: Breakout) =>
 export const removeBreakout = (query: SQ, index: number)                     => setBreakoutClause(query, B.removeBreakout(query.breakout, index));
 export const clearBreakouts = (query: SQ)                                    => setBreakoutClause(query, B.clearBreakouts(query.breakout));
 
+export const getBreakoutFields = (query: SQ, tableMetadata: TableMetadata) => B.getBreakoutFields(query.breakout, tableMetadata);
+
 // FILTER
 
 export const getFilters   = (query: SQ)                                 => F.getFilters(query.filter);
@@ -61,6 +66,19 @@ export const clearOrderBy  = (query: SQ)                                   => se
 export const updateLimit = (query: SQ, limit: LimitClause) => setLimitClause(query, L.updateLimit(query.limit, limit));
 export const clearLimit = (query: SQ) => setLimitClause(query, L.clearLimit(query.limit));
 
+// EXPRESSIONS
+
+export const getExpressions     = (query: SQ) => E.getExpressions(query.expressions);
+export const getExpressionsList = (query: SQ) => E.getExpressionsList(query.expressions);
+export const addExpression    = (query: SQ, name: ExpressionName, expression: Expression) =>
+    setExpressionClause(query, E.addExpression(query.expressions, name, expression));
+export const updateExpression = (query: SQ, name: ExpressionName, expression: Expression, oldName: ExpressionName) =>
+    setExpressionClause(query, E.updateExpression(query.expressions, name, expression, oldName));
+export const removeExpression = (query: SQ, name: ExpressionName) =>
+    setExpressionClause(query, E.removeExpression(query.expressions, name));
+export const clearExpression  = (query: SQ) =>
+    setExpressionClause(query, E.clearExpressions(query.expressions));
+
 // we can enforce various constraints in these functions:
 
 function setAggregationClause(query: SQ, aggregationClause: ?AggregationClause): SQ {
@@ -95,9 +113,15 @@ function setOrderByClause(query: SQ, orderByClause: ?OrderByClause): SQ {
 function setLimitClause(query: SQ, limitClause: ?LimitClause): SQ {
     return setClause("limit", query, limitClause);
 }
+function setExpressionClause(query: SQ, expressionClause: ?ExpressionClause): SQ {
+    if (expressionClause && Object.keys(expressionClause).length === 0) {
+        expressionClause = null;
+    }
+    return setClause("expressions", query, expressionClause);
+}
 
 // TODO: remove mutation
-type FilterClauseName = "filter"|"aggregation"|"breakout"|"order_by"|"limit";
+type FilterClauseName = "filter"|"aggregation"|"breakout"|"order_by"|"limit"|"expressions";
 function setClause(clauseName: FilterClauseName, query: SQ, clause: ?any): SQ {
     if (clause == null) {
         delete query[clauseName];
diff --git a/frontend/src/metabase/lib/query/table.js b/frontend/src/metabase/lib/query/table.js
new file mode 100644
index 0000000000000000000000000000000000000000..96d9786e175aca8c45a0c442a5a0f2de71b2d73b
--- /dev/null
+++ b/frontend/src/metabase/lib/query/table.js
@@ -0,0 +1,14 @@
+/* @flow weak */
+
+import _ from "underscore";
+
+export function getField(table, fieldId) {
+    if (table) {
+        // sometimes we populate fields_lookup, sometimes we don't :(
+        if (table.fields_lookup) {
+            return table.fields_lookup[fieldId];
+        } else {
+            return _.findWhere(table.fields, { id: fieldId });
+        }
+    }
+}
diff --git a/frontend/src/metabase/lib/query_time.js b/frontend/src/metabase/lib/query_time.js
index ac1b93f6dc403595fff1999c1345b6ed51110503..a57b5509c396c3b25c3aa6e7245201e5777a3d0e 100644
--- a/frontend/src/metabase/lib/query_time.js
+++ b/frontend/src/metabase/lib/query_time.js
@@ -2,6 +2,7 @@ import moment from "moment";
 import inflection from "inflection";
 
 import { mbqlEq } from "metabase/lib/query/util";
+import { formatTimeWithUnit } from "metabase/lib/formatting";
 
 export function computeFilterTimeRange(filter) {
     let expandedFilter;
@@ -110,7 +111,9 @@ export function generateTimeIntervalDescription(n, unit) {
 export function generateTimeValueDescription(value, bucketing) {
     if (typeof value === "string") {
         let m = moment(value);
-        if(m.hours() || m.minutes()) {
+        if (bucketing) {
+            return formatTimeWithUnit(value, bucketing);
+        } else if (m.hours() || m.minutes()) {
             return m.format("MMMM D, YYYY hh:mm a");
         } else {
             return m.format("MMMM D, YYYY");
@@ -169,13 +172,22 @@ export function parseFieldBucketing(field, defaultUnit = null) {
     return defaultUnit;
 }
 
+// returns field with "datetime-field" removed
 export function parseFieldTarget(field) {
+    if (mbqlEq(field[0], "datetime-field")) {
+        return field[1];
+    } else {
+        return field;
+    }
+}
+
+export function parseFieldTargetId(field) {
     if (Number.isInteger(field)) return field;
 
     if (Array.isArray(field)) {
         if (mbqlEq(field[0], "field-id"))       return field[1];
         if (mbqlEq(field[0], "fk->"))           return field[1];
-        if (mbqlEq(field[0], "datetime-field")) return parseFieldTarget(field[1]);
+        if (mbqlEq(field[0], "datetime-field")) return parseFieldTargetId(field[1]);
     }
 
     console.warn("Unknown field format", field);
diff --git a/frontend/src/metabase/lib/redux.js b/frontend/src/metabase/lib/redux.js
index dd593ad9aae7b778f773ce7f23f52683ec8084f7..9f2523301585ab8b60fbf059b699d7bcb69a7825 100644
--- a/frontend/src/metabase/lib/redux.js
+++ b/frontend/src/metabase/lib/redux.js
@@ -2,35 +2,12 @@ import moment from "moment";
 import _ from "underscore";
 import { getIn } from "icepick";
 
-import { createStore as originalCreateStore, applyMiddleware, compose } from "redux";
-import promise from 'redux-promise';
-import thunk from "redux-thunk";
-import createLogger from "redux-logger";
-
-import createHistory from "history/createBrowserHistory";
-
-import { reduxReactRouter } from 'redux-router';
-
 import { setRequestState, clearRequestState } from "metabase/redux/requests";
 
 // convienence
 export { combineReducers } from "redux";
 export { handleActions, createAction } from "redux-actions";
 
-import { DEBUG } from "metabase/lib/debug";
-
-let middleware = [thunk, promise];
-if (DEBUG) {
-    middleware.push(createLogger());
-}
-
-// common createStore with middleware applied
-export const createStore = compose(
-  applyMiddleware(...middleware),
-  reduxReactRouter({ createHistory }),
-  window.devToolsExtension ? window.devToolsExtension() : f => f
-)(originalCreateStore);
-
 // similar to createAction but accepts a (redux-thunk style) thunk and dispatches based on whether
 // the promise returned from the thunk resolves or rejects, similar to redux-promise
 export function createThunkAction(actionType, actionThunkCreator) {
diff --git a/frontend/src/metabase/lib/schema_metadata.js b/frontend/src/metabase/lib/schema_metadata.js
index af86137411499c5337cd324a9885e3809ab52611..ef30ddb624d8ab28c8eae82d1abcabcec8de4879 100644
--- a/frontend/src/metabase/lib/schema_metadata.js
+++ b/frontend/src/metabase/lib/schema_metadata.js
@@ -115,9 +115,12 @@ export const isNumericBaseType = (field) => isa(field && field.base_type, TYPE.N
 // ZipCode, ID, etc derive from Number but should not be formatted as numbers
 export const isNumber = (field) => field && isNumericBaseType(field) && (field.special_type == null || field.special_type === TYPE.Number);
 
-export const isCoordinate = (field) => isa(field && field.special_type, TYPE.Coordinate);
-export const isLatitude = (field) => isa(field && field.special_type, TYPE.Latitude);
-export const isLongitude = (field) => isa(field && field.special_type, TYPE.Longitude);
+export const isAddress      = (field) => isa(field && field.special_type, TYPE.Address);
+export const isState        = (field) => isa(field && field.special_type, TYPE.State);
+export const isCountry      = (field) => isa(field && field.special_type, TYPE.Country);
+export const isCoordinate   = (field) => isa(field && field.special_type, TYPE.Coordinate);
+export const isLatitude     = (field) => isa(field && field.special_type, TYPE.Latitude);
+export const isLongitude    = (field) => isa(field && field.special_type, TYPE.Longitude);
 
 // operator argument constructors:
 
diff --git a/frontend/src/metabase/lib/urls.js b/frontend/src/metabase/lib/urls.js
index 286969c8bc881a69afe11bd01dd3b358a1bba27f..7c2eba73166aeef3535ac9f4ee9e9f3b995fba8d 100644
--- a/frontend/src/metabase/lib/urls.js
+++ b/frontend/src/metabase/lib/urls.js
@@ -1,60 +1,71 @@
+import { serializeCardForUrl } from "metabase/lib/card";
+
 // provides functions for building urls to things we care about
-var Urls = {
-    card: function(card_id) {
-        // NOTE that this is for an ephemeral card link, not an editable card
-        return "/card/"+card_id;
-    },
 
-    dashboard: function(dashboard_id) {
-        return "/dash/"+dashboard_id;
-    },
+export function question(cardId, cardOrHash = "") {
+    if (cardOrHash && typeof cardOrHash === "object") {
+        cardOrHash = serializeCardForUrl(cardOrHash);
+    }
+    if (cardOrHash && cardOrHash.charAt(0) !== "#") {
+        cardOrHash = "#" + cardOrHash;
+    }
+    // NOTE that this is for an ephemeral card link, not an editable card
+    return cardId != null
+        ? `/question/${cardId}${cardOrHash}`
+        : `/question${cardOrHash}`;
+}
 
-    modelToUrl: function(model, model_id) {
-        switch (model) {
-            case "card":      return Urls.card(model_id);
-            case "dashboard": return Urls.dashboard(model_id);
-            case "pulse":     return Urls.pulse(model_id);
-            default:          return null;
-        }
-    },
+export function dashboard(dashboardId) {
+    return `/dashboard/${dashboardId}`;
+}
 
-    pulse: function(pulse_id) {
-        return "/pulse/#"+pulse_id;
-    },
+export function modelToUrl(model, modelId) {
+    switch (model) {
+        case "card":
+            return question(modelId);
+        case "dashboard":
+            return dashboard(modelId);
+        case "pulse":
+            return pulse(modelId);
+        default:
+            return null;
+    }
+}
 
-    tableRowsQuery: function(database_id, table_id, metric_id, segment_id) {
-        let url = "/q#?db="+database_id+"&table="+table_id;
+export function pulse(pulseId) {
+    return `/pulse/#${pulseId}`;
+}
 
-        if (metric_id) {
-            url = url + "&metric="+metric_id;
-        }
+export function tableRowsQuery(databaseId, tableId, metricId, segmentId) {
+    let query = `?db=${databaseId}&table=${tableId}`;
 
-        if (segment_id) {
-            url = url + "&segment="+segment_id;
-        }
+    if (metricId) {
+        query += `&metric=${metricId}`;
+    }
 
-        return url;
-    },
+    if (segmentId) {
+        query += `&segment=${segmentId}`;
+    }
 
-    collection(collection) {
-        return `/questions/collections/${encodeURIComponent(collection.slug)}`;
-    },
+    return question(null, query);
+}
 
-    label(label) {
-        return `/questions/search?label=${encodeURIComponent(label.slug)}`;
-    },
+export function collection(collection) {
+    return `/questions/collections/${encodeURIComponent(collection.slug)}`;
+}
 
-    publicCard(uuid, type = null) {
-        return `/public/question/${uuid}` + (type ? `.${type}` : ``);
-    },
+export function label(label) {
+    return `/questions/search?label=${encodeURIComponent(label.slug)}`;
+}
 
-    publicDashboard(uuid) {
-        return `/public/dashboard/${uuid}`;
-    },
+export function publicCard(uuid, type = null) {
+    return `/public/question/${uuid}` + (type ? `.${type}` : ``);
+}
 
-    embedCard(token, type = null) {
-        return `/embed/question/${token}` + (type ? `.${type}` : ``);
-    },
+export function publicDashboard(uuid) {
+    return `/public/dashboard/${uuid}`;
 }
 
-export default Urls;
+export function embedCard(token, type = null) {
+    return `/embed/question/${token}` + (type ? `.${type}` : ``);
+}
diff --git a/frontend/src/metabase/meta/Card.js b/frontend/src/metabase/meta/Card.js
index 8c2316dffad9f7b50cbf00f2b23bc51d61a51e10..996ae2972b7e5ae3d108b6dbe593f85603f8a99d 100644
--- a/frontend/src/metabase/meta/Card.js
+++ b/frontend/src/metabase/meta/Card.js
@@ -43,8 +43,8 @@ export function isNative(card: Card): bool {
 
 export function canRun(card: Card): bool {
     if (card.dataset_query.type === "query") {
-        const query : StructuredQuery = card.dataset_query.query;
-        return query && query.source_table != undefined && Query.hasValidAggregation(query);
+        const query = getQuery(card);
+        return query != null && query.source_table != undefined && Query.hasValidAggregation(query);
     } else if (card.dataset_query.type === "native") {
         const native : NativeQuery = card.dataset_query.native;
         return native && card.dataset_query.database != undefined && native.query !== "";
@@ -90,29 +90,25 @@ export function applyParameters(
     datasetQuery.parameters = [];
     for (const parameter of parameters || []) {
         let value = parameterValues[parameter.id];
+        if (value == null) {
+            continue;
+        }
 
-        // dashboards
         const mapping = _.findWhere(parameterMappings, { card_id: card.id, parameter_id: parameter.id });
-        if (value != null && mapping) {
+        if (mapping) {
+            // mapped target, e.x. on a dashboard
             datasetQuery.parameters.push({
                 type: parameter.type,
                 target: mapping.target,
                 value: value
             });
-        }
-
-        // SQL parameters
-        if (datasetQuery.type === "native") {
-            let tag = _.findWhere(datasetQuery.native.template_tags, { id: parameter.id });
-            if (tag) {
-                datasetQuery.parameters.push({
-                    type: parameter.type,
-                    target: tag.type === "dimension" ?
-                        ["dimension", ["template-tag", tag.name]]:
-                        ["variable", ["template-tag", tag.name]],
-                    value: value
-                });
-            }
+        } else if (parameter.target) {
+            // inline target, e.x. on a card
+            datasetQuery.parameters.push({
+                type: parameter.type,
+                target: parameter.target,
+                value: value
+            });
         }
     }
 
diff --git a/frontend/src/metabase/meta/Dashboard.js b/frontend/src/metabase/meta/Dashboard.js
index 8d5d60f0bb5c5bd054ecbdc94f7894e926976769..b03eacc7052c3aa1a49af63dd9b710b8e15ed86b 100644
--- a/frontend/src/metabase/meta/Dashboard.js
+++ b/frontend/src/metabase/meta/Dashboard.js
@@ -6,7 +6,7 @@ import type Field from "./metadata/Field";
 import type { FieldId } from "./types/Field";
 import type { TemplateTag } from "./types/Query";
 import type { Card } from "./types/Card";
-import type { ParameterOption, Parameter, ParameterMappingUIOption, ParameterMappingTarget, DimensionTarget, VariableTarget } from "./types/Dashboard";
+import type { ParameterOption, Parameter, ParameterType, ParameterMappingUIOption, ParameterMappingTarget, DimensionTarget, VariableTarget } from "./types/Dashboard";
 
 import { getTemplateTags } from "./Card";
 
@@ -43,6 +43,12 @@ export const PARAMETER_OPTIONS: Array<ParameterOption> = [
         name: "Relative Date",
         description: "Like \"the last 7 days\" or \"this month\""
     },
+    {
+        type: "date/all-options",
+        name: "Date Filter",
+        menuName: "All Options",
+        description: "Contains all of the above"
+    },
     {
         type: "location/city",
         name: "City"
@@ -217,14 +223,18 @@ export function getParameterMappingTargetField(metadata: Metadata, card: Card, t
     return null;
 }
 
-function fieldFilterForParameter(parameter: Parameter): FieldFilter {
-    const [type] = parameter.type.split("/");
+function fieldFilterForParameter(parameter: Parameter) {
+    return fieldFilterForParameterType(parameter.type);
+}
+
+export function fieldFilterForParameterType(parameterType: ParameterType): FieldFilter {
+    const [type] = parameterType.split("/");
     switch (type) {
         case "date":        return (field: Field) => field.isDate();
         case "id":          return (field: Field) => field.isID();
         case "category":    return (field: Field) => field.isCategory();
     }
-    switch (parameter.type) {
+    switch (parameterType) {
         case "location/city":     return (field: Field) => isa(field.special_type, TYPE.City);
         case "location/state":    return (field: Field) => isa(field.special_type, TYPE.State);
         case "location/zip_code": return (field: Field) => isa(field.special_type, TYPE.ZipCode);
@@ -233,6 +243,10 @@ function fieldFilterForParameter(parameter: Parameter): FieldFilter {
     return (field: Field) => false;
 }
 
+export function parameterOptionsForField(field: Field): ParameterOption[] {
+    return PARAMETER_OPTIONS.filter(option => fieldFilterForParameterType(option.type)(field));
+}
+
 function tagFilterForParameter(parameter: Parameter): TemplateTagFilter {
     const [type, subtype] = parameter.type.split("/");
     switch (type) {
diff --git a/frontend/src/metabase/meta/Parameter.js b/frontend/src/metabase/meta/Parameter.js
index 54db7ae4629c8e7f5a20696a479a4e7d665d526b..130ac93e4bdde4ee476af69733ccfc4303a35025 100644
--- a/frontend/src/metabase/meta/Parameter.js
+++ b/frontend/src/metabase/meta/Parameter.js
@@ -7,12 +7,15 @@ export type ParameterValues = {
     [id: ParameterId]: string
 };
 
+// NOTE: this should mirror `template-tag-parameters` in src/metabase/api/embed.clj
 export function getTemplateTagParameters(tags: TemplateTag[]): Parameter[] {
-    return tags.filter(tag => tag.type != null && tag.type !== "dimension")
+    return tags.filter(tag => tag.type != null && (tag.widget_type || tag.type !== "dimension"))
         .map(tag => ({
             id: tag.id,
-            type: tag.type === "date" ? "date/single" : "category",
-            target: ["variable", ["template-tag", tag.name]],
+            type: tag.widget_type || (tag.type === "date" ? "date/single" : "category"),
+            target: tag.type === "dimension" ?
+                ["dimension", ["template-tag", tag.name]]:
+                ["variable", ["template-tag", tag.name]],
             name: tag.display_name,
             slug: tag.name,
             default: tag.default
diff --git a/frontend/src/metabase/meta/types/Collection.js b/frontend/src/metabase/meta/types/Collection.js
new file mode 100644
index 0000000000000000000000000000000000000000..13095f04db1666fee39fcd8e7761753e73884ddb
--- /dev/null
+++ b/frontend/src/metabase/meta/types/Collection.js
@@ -0,0 +1,9 @@
+/* @flow */
+
+export type CollectionId = number;
+
+export type Collection = {
+    id: CollectionId,
+    name: string,
+    color: string,
+}
diff --git a/frontend/src/metabase/meta/types/Dataset.js b/frontend/src/metabase/meta/types/Dataset.js
index 4f8c9f95e4671f1a389bd451a63ccef66218058e..90bbe3f1ed40bf5d0e1212b37633ed38fd49ed57 100644
--- a/frontend/src/metabase/meta/types/Dataset.js
+++ b/frontend/src/metabase/meta/types/Dataset.js
@@ -11,7 +11,8 @@ export type Column = {
     name: ColumnName,
     display_name: string,
     base_type: string,
-    special_type: ?string
+    special_type: ?string,
+    source?: "fields"|"aggregation"|"breakout"
 };
 
 export type ISO8601Times = string;
diff --git a/frontend/src/metabase/meta/types/Metadata.js b/frontend/src/metabase/meta/types/Metadata.js
index 58872755f33dea358d1472d83ea4d29b14f2b435..4fb281711ba440ff13d1b42fdb1b7dedc5204dac 100644
--- a/frontend/src/metabase/meta/types/Metadata.js
+++ b/frontend/src/metabase/meta/types/Metadata.js
@@ -35,7 +35,32 @@ export type FieldMetadata = Field & {
     operators_lookup: { [name: string]: Operator }
 }
 
+export type AggregationOption = {
+    name: string,
+    short: string,
+    fields: Field[],
+    validFieldsFilter: (fields: Field[]) => Field[]
+}
+
+export type BreakoutOptions = {
+    name: string,
+    short: string,
+    fields: Field[],
+    validFieldsFilter: (fields: Field[]) => Field[]
+}
+
 export type TableMetadata = Table & {
     segments: Segment[],
-    fields: FieldMetadata[]
+    fields: FieldMetadata[],
+    aggregation_options: AggregationOption[],
+    breakout_options: BreakoutOptions
 }
+
+export type FieldOptions = {
+    count: number,
+    fields: Field[],
+    fks: {
+        field: Field,
+        fields: Field[]
+    }
+};
diff --git a/frontend/src/metabase/meta/types/Parameter.js b/frontend/src/metabase/meta/types/Parameter.js
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/frontend/src/metabase/meta/types/Query.js b/frontend/src/metabase/meta/types/Query.js
index 6d3c52333323bacdc0b7eee9c6840d1db0b77e2b..8f4ab74316c2417866e6f4efea6298e093391a60 100644
--- a/frontend/src/metabase/meta/types/Query.js
+++ b/frontend/src/metabase/meta/types/Query.js
@@ -3,6 +3,7 @@
 import type { TableId } from "./Table";
 import type { FieldId } from "./Field";
 import type { SegmentId } from "./Segment";
+import type { ParameterType } from "./Dashboard";
 
 export type MetricId = number;
 
@@ -27,6 +28,8 @@ export type TemplateTag = {
     display_name: string,
     type:         string,
     dimension?:   ["field-id", number],
+    widget_type?: ParameterType,
+    required?:    boolean,
     default?:     string,
 };
 
diff --git a/frontend/src/metabase/meta/types/Table.js b/frontend/src/metabase/meta/types/Table.js
index 002ebe272f476e49235eb4c99d2100d6fbfbb56a..51ca40ef2b89d44d2ee8b1ce0bfd80b9e865682b 100644
--- a/frontend/src/metabase/meta/types/Table.js
+++ b/frontend/src/metabase/meta/types/Table.js
@@ -1,6 +1,7 @@
 /* @flow */
 
 import type { Field } from "./Field";
+import type { DatabaseId } from "./Database";
 
 export type TableId = number;
 export type SchemaName = string;
@@ -9,6 +10,8 @@ export type SchemaName = string;
 export type Table = {
     id: TableId,
 
+    db_id: DatabaseId,
+
     name: string,
     display_name: string,
     schema?: SchemaName,
diff --git a/frontend/src/metabase/meta/types/Visualization.js b/frontend/src/metabase/meta/types/Visualization.js
new file mode 100644
index 0000000000000000000000000000000000000000..0c4631b4373e46e91fb290aa6e7be81e843eb052
--- /dev/null
+++ b/frontend/src/metabase/meta/types/Visualization.js
@@ -0,0 +1,85 @@
+/* @flow */
+
+import type { DatasetData, Column } from "metabase/meta/types/Dataset";
+import type { Card, VisualizationSettings } from "metabase/meta/types/Card";
+import type { TableMetadata } from "metabase/meta/types/Metadata";
+
+export type ActionCreator = (props: ClickActionProps) => ?ClickAction
+
+export type QueryMode = {
+    name: string,
+    actions: ActionCreator[],
+    drills: ActionCreator[]
+}
+
+export type HoverData = Array<{ key: string, value: any, col?: Column }>;
+
+export type HoverObject = {
+    index?: number,
+    axisIndex?: number,
+    data?: HoverData,
+    element?: ?HTMLElement,
+    event?: MouseEvent,
+}
+
+export type DimensionValue = {
+    value: Value,
+    column: Column
+};
+
+export type ClickObject = {
+    value?: Value,
+    column: Column,
+    dimensions?: DimensionValue[],
+    event?: MouseEvent,
+    element?: HTMLElement,
+}
+
+export type ClickAction = {
+    title: any, // React Element
+    icon?: string,
+    popover?: (props: ClickActionPopoverProps) => any, // React Element
+    card?: () => ?Card
+}
+
+export type ClickActionProps = {
+    card: Card,
+    tableMetadata: TableMetadata,
+    clicked?: ClickObject
+}
+
+export type ClickActionPopoverProps = {
+    onChangeCardAndRun: (card: ?Card) => void,
+    onClose: () => void,
+}
+
+// type Visualization = Component<*, VisualizationProps, *>;
+
+// $FlowFixMe
+export type Series = { card: Card, data: DatasetData }[] & { _raw: Series }
+
+export type VisualizationProps = {
+    series: Series,
+    card: Card,
+    data: DatasetData,
+    settings: VisualizationSettings,
+
+    className?: string,
+    gridSize: ?{
+        width: number,
+        height: number
+    },
+
+    showTitle: boolean,
+    isDashboard: boolean,
+    isEditing: boolean,
+    actionButtons: Node,
+    linkToCard?: bool,
+
+    hovered: ?HoverObject,
+    onHoverChange: (?HoverObject) => void,
+    onVisualizationClick: (?ClickObject) => void,
+    visualizationIsClickable: (?ClickObject) => boolean,
+
+    onUpdateVisualizationSettings: ({ [key: string]: any }) => void
+}
diff --git a/frontend/src/metabase/nav/containers/DashboardsDropdown.jsx b/frontend/src/metabase/nav/containers/DashboardsDropdown.jsx
index b902a25096e2c25ff4cdac9d6854c6b69643a88c..425c4d220862d0d318126d3c532ed7560f8ca26f 100644
--- a/frontend/src/metabase/nav/containers/DashboardsDropdown.jsx
+++ b/frontend/src/metabase/nav/containers/DashboardsDropdown.jsx
@@ -3,12 +3,13 @@ import { connect } from "react-redux";
 import { Link } from "react-router";
 
 import OnClickOutsideWrapper from 'metabase/components/OnClickOutsideWrapper.jsx';
-
-import MetabaseAnalytics from "metabase/lib/analytics";
 import CreateDashboardModal from "metabase/components/CreateDashboardModal.jsx";
 import Modal from "metabase/components/Modal.jsx";
 import ConstrainToScreen from "metabase/components/ConstrainToScreen";
 
+import MetabaseAnalytics from "metabase/lib/analytics";
+import * as Urls from "metabase/lib/urls";
+
 import _ from "underscore";
 import cx from "classnames";
 
@@ -62,7 +63,7 @@ export default class DashboardsDropdown extends Component {
         try {
             let action = await createDashboard(newDashboard, true);
             // FIXME: this doesn't feel right...
-            this.props.onChangeLocation(`/dash/${action.payload.id}`);
+            this.props.onChangeLocation(Urls.dashboard(action.payload.id));
         } catch (e) {
             console.log("createDashboard failed", e);
         }
@@ -137,7 +138,7 @@ export default class DashboardsDropdown extends Component {
                                     <ul className="NavDropdown-content-layer">
                                         { dashboards.map(dash =>
                                             <li key={dash.id} className="block">
-                                                <Link to={"/dash/"+dash.id} data-metabase-event={"Navbar;Dashboard Dropdown;Open Dashboard;"+dash.id} className="Dropdown-item block text-white no-decoration" onClick={this.closeDropdown}>
+                                                <Link to={Urls.dashboard(dash.id)} data-metabase-event={"Navbar;Dashboard Dropdown;Open Dashboard;"+dash.id} className="Dropdown-item block text-white no-decoration" onClick={this.closeDropdown}>
                                                     <div className="flex text-bold">
                                                         {dash.name}
                                                     </div>
diff --git a/frontend/src/metabase/nav/containers/Navbar.jsx b/frontend/src/metabase/nav/containers/Navbar.jsx
index 74520d03ff4d49a44a9bc1a91e2df4f3bf0ee24c..3dc5c7f4cef6f4ebf7bba46752d1e5fdf9baa5bb 100644
--- a/frontend/src/metabase/nav/containers/Navbar.jsx
+++ b/frontend/src/metabase/nav/containers/Navbar.jsx
@@ -11,6 +11,8 @@ import LogoIcon from "metabase/components/LogoIcon.jsx";
 import DashboardsDropdown from "metabase/nav/containers/DashboardsDropdown.jsx";
 import ProfileLink from "metabase/nav/components/ProfileLink.jsx";
 
+import * as Urls from "metabase/lib/urls";
+
 import { getPath, getContext, getUser } from "../selectors";
 
 const mapStateToProps = (state, props) => ({
@@ -115,7 +117,13 @@ export default class Navbar extends Component {
                     </li>
                     <li className="pl3">
                         <DashboardsDropdown {...this.props}>
-                            <a data-metabase-event={"Navbar;Dashboard Dropdown;Toggle"} style={this.styles.navButton} className={cx("NavDropdown-button NavItem text-white text-bold cursor-pointer px2 flex align-center transition-background", {"NavItem--selected": this.isActive("/dash/")})}>
+                            <a
+                                data-metabase-event={"Navbar;Dashboard Dropdown;Toggle"}
+                                style={this.styles.navButton}
+                                className={cx("NavDropdown-button NavItem text-white text-bold cursor-pointer px2 flex align-center transition-background", {
+                                    "NavItem--selected": this.isActive("/dashboard/")
+                                })}
+                            >
                                 <span className="NavDropdown-button-layer">
                                     Dashboards
                                     <Icon className="ml1" name={'chevrondown'} size={8}></Icon>
@@ -133,7 +141,7 @@ export default class Navbar extends Component {
                         <Link to="/reference/guide" data-metabase-event={"Navbar;DataReference"} style={this.styles.navButton} className={cx("NavItem cursor-pointer text-white text-bold no-decoration flex align-center px2 transition-background")} activeClassName="NavItem--selected">Data Reference</Link>
                     </li>
                     <li className="pl3">
-                        <Link to="/q" data-metabase-event={"Navbar;New Question"} style={this.styles.newQuestion} className="NavNewQuestion rounded inline-block bg-white text-brand text-bold cursor-pointer px2 no-decoration transition-all">New <span className="hide sm-show">Question</span></Link>
+                        <Link to={Urls.question()} data-metabase-event={"Navbar;New Question"} style={this.styles.newQuestion} className="NavNewQuestion rounded inline-block bg-white text-brand text-bold cursor-pointer px2 no-decoration transition-all">New <span className="hide sm-show">Question</span></Link>
                     </li>
                     <li className="flex-align-right transition-background">
                         <div className="inline-block text-white"><ProfileLink {...this.props}></ProfileLink></div>
diff --git a/frontend/src/metabase/public/components/EmbedFrame.jsx b/frontend/src/metabase/public/components/EmbedFrame.jsx
index d800987f0d574a7c898537e47878a1354d8105ac..5dcfb7f14379029b4a9c5d5de2bb3a15289bca96 100644
--- a/frontend/src/metabase/public/components/EmbedFrame.jsx
+++ b/frontend/src/metabase/public/components/EmbedFrame.jsx
@@ -1,7 +1,6 @@
 /* @flow */
 
 import React, { Component, PropTypes } from "react";
-import ReactDOM from "react-dom";
 import { withRouter } from "react-router"; import { IFRAMED } from "metabase/lib/dom";
 
 import Parameters from "metabase/dashboard/containers/Parameters";
diff --git a/frontend/src/metabase/pulse/actions.js b/frontend/src/metabase/pulse/actions.js
index 35833cb196b8fe0dd00937519a904532656674e3..72d1270716297946f3c070539aa3dd5400d750a3 100644
--- a/frontend/src/metabase/pulse/actions.js
+++ b/frontend/src/metabase/pulse/actions.js
@@ -40,7 +40,8 @@ export const setEditingPulse = createThunkAction(SET_EDITING_PULSE, function(id)
         return {
             name: null,
             cards: [],
-            channels: []
+            channels: [],
+            skip_if_empty: false,
         }
     };
 });
diff --git a/frontend/src/metabase/pulse/components/PulseEdit.jsx b/frontend/src/metabase/pulse/components/PulseEdit.jsx
index a76852c8c9ac4283dde35fcceb94b8315f694c97..7d50c9bae6f5d01a32bb07201cf5dd07a071fc45 100644
--- a/frontend/src/metabase/pulse/components/PulseEdit.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEdit.jsx
@@ -5,6 +5,7 @@ import { Link } from "react-router";
 import PulseEditName from "./PulseEditName.jsx";
 import PulseEditCards from "./PulseEditCards.jsx";
 import PulseEditChannels from "./PulseEditChannels.jsx";
+import PulseEditSkip from "./PulseEditSkip.jsx";
 import WhatsAPulse from "./WhatsAPulse.jsx";
 
 import ActionButton from "metabase/components/ActionButton.jsx";
@@ -116,6 +117,7 @@ export default class PulseEdit extends Component {
                     <PulseEditName {...this.props} setPulse={this.setPulse} />
                     <PulseEditCards {...this.props} setPulse={this.setPulse} />
                     <PulseEditChannels {...this.props} setPulse={this.setPulse} pulseIsValid={isValid} />
+                    <PulseEditSkip {...this.props} setPulse={this.setPulse} />
                     { pulse && pulse.id != null &&
                         <div className="DangerZone mb2 p3 rounded bordered relative">
                             <h3 className="text-error absolute top bg-white px1" style={{ marginTop: "-12px" }}>Danger Zone</h3>
diff --git a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
index da11f0d023368c0feb9d7fff10119ed096f8a04f..c92a3215312688b996aea6152064d99f87ea7cc5 100644
--- a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
@@ -38,7 +38,8 @@ export default class PulseEditChannels extends Component {
         user: PropTypes.object.isRequired,
         userList: PropTypes.array.isRequired,
         setPulse: PropTypes.func.isRequired,
-        testPulse: PropTypes.func.isRequired
+        testPulse: PropTypes.func.isRequired,
+        cardPreviews: PropTypes.array
     };
     static defaultProps = {};
 
@@ -134,6 +135,15 @@ export default class PulseEditChannels extends Component {
         return this.props.testPulse({ ...this.props.pulse, channels: [channel] });
     }
 
+    willPulseSkip = () => {
+        let cards = _.pluck(this.props.pulse.cards, 'id');
+        let cardPreviews = this.props.cardPreviews;
+        let previews = _.map(cards, function (id) { return _.find(cardPreviews, function(card){ return (id == card.id);})});
+        let types = _.pluck(previews, 'pulse_card_type');
+        let empty = _.isEqual( _.uniq(types), ["empty"]);
+        return (empty && this.props.pulse.skip_if_empty);
+    }
+
     renderFields(channel, index, channelSpec) {
         return (
             <div>
@@ -142,7 +152,7 @@ export default class PulseEditChannels extends Component {
                         <span className="h4 text-bold mr1">{field.displayName}</span>
                         { field.type === "select" ?
                             <Select
-                                className="h4 text-bold"
+                                className="h4 text-bold bg-white"
                                 value={channel.details[field.name]}
                                 options={field.options}
                                 optionNameFn={o => o}
@@ -194,7 +204,8 @@ export default class PulseEditChannels extends Component {
                             "Send to  " + channelSpec.name + " now"}
                         activeText="Sending…"
                         failedText="Sending failed"
-                        successText="Pulse sent"
+                        successText={ this.willPulseSkip() ?  "Didn’t send because the pulse has no results." : "Pulse sent"}
+                        forceActiveStyle={ this.willPulseSkip() }
                     />
                 </div>
             </li>
diff --git a/frontend/src/metabase/pulse/components/PulseEditSkip.jsx b/frontend/src/metabase/pulse/components/PulseEditSkip.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..4c17054314ebac9831fd5b0dfc43c0f5a5b6ac10
--- /dev/null
+++ b/frontend/src/metabase/pulse/components/PulseEditSkip.jsx
@@ -0,0 +1,28 @@
+import React, { Component, PropTypes } from "react";
+
+import Toggle from "metabase/components/Toggle.jsx";
+
+export default class PulseEditSkip extends Component {
+    static propTypes = {
+        pulse: PropTypes.object.isRequired,
+        setPulse: PropTypes.func.isRequired,
+    };
+
+    toggle = () => {
+        const { pulse, setPulse } = this.props;
+        setPulse({ ...pulse, skip_if_empty: !pulse.skip_if_empty });
+    }
+
+    render() {
+        const { pulse } = this.props;
+        return (
+            <div className="py1">
+                <h2>Skip if no results</h2>
+                <p className="mt1 h4 text-bold text-grey-3">Skip a scheduled Pulse if none of its questions have any results.</p>
+                <div className="my3">
+                    <Toggle value={pulse.skip_if_empty || false} onChange={this.toggle} />
+                </div>
+            </div>
+        );
+    }
+}
diff --git a/frontend/src/metabase/pulse/components/PulseListItem.jsx b/frontend/src/metabase/pulse/components/PulseListItem.jsx
index 7429928e757872a2c685fb3d3549fe073191833a..47c0e3b7e2c6468f17cdcef9b4fda291c0572723 100644
--- a/frontend/src/metabase/pulse/components/PulseListItem.jsx
+++ b/frontend/src/metabase/pulse/components/PulseListItem.jsx
@@ -5,7 +5,7 @@ import { Link } from "react-router";
 
 import cx from "classnames";
 
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 import PulseListChannel from "./PulseListChannel.jsx";
 
 export default class PulseListItem extends Component {
@@ -43,7 +43,7 @@ export default class PulseListItem extends Component {
                 <ol className="mb2 px4 flex flex-wrap">
                     { pulse.cards.map((card, index) =>
                         <li key={index} className="mr1 mb1">
-                            <Link to={Urls.card(card.id)} className="Button">
+                            <Link to={Urls.question(card.id)} className="Button">
                                 {card.name}
                             </Link>
                         </li>
diff --git a/frontend/src/metabase/pulse/components/RecipientPicker.jsx b/frontend/src/metabase/pulse/components/RecipientPicker.jsx
index 3b69caf3a5aea5dfd6b7f6fe977278bbb5bec09e..67db8b2cd3cca1a4e5706a7d378f8ce417933624 100644
--- a/frontend/src/metabase/pulse/components/RecipientPicker.jsx
+++ b/frontend/src/metabase/pulse/components/RecipientPicker.jsx
@@ -154,7 +154,7 @@ export default class RecipientPicker extends Component {
         let { recipients } = this.props;
 
         return (
-            <ul className={cx("px1 pb1 bordered rounded flex flex-wrap", { "input--focus": this.state.focused })} onMouseDownCapture={this.onMouseDownCapture}>
+            <ul className={cx("px1 pb1 bordered rounded flex flex-wrap bg-white", { "input--focus": this.state.focused })} onMouseDownCapture={this.onMouseDownCapture}>
                 {recipients.map((recipient, index) =>
                     <li key={index} className="mr1 py1 pl1 mt1 rounded bg-grey-1">
                         <span className="h4 text-bold">{recipient.common_name || recipient.email}</span>
@@ -163,12 +163,11 @@ export default class RecipientPicker extends Component {
                         </a>
                     </li>
                 )}
-                <li className="flex-full mr1 py1 pl1 mt1" style={{ "minWidth": " 100px" }}>
+                <li className="flex-full mr1 py1 pl1 mt1 bg-white" style={{ "minWidth": " 100px" }}>
                     <input
                         ref="input"
                         type="text"
                         className="full h4 text-bold text-default no-focus borderless"
-                        style={{"backgroundColor": "transparent"}}
                         placeholder={recipients.length === 0 ? "Enter email addresses you'd like this data to go to" : null}
                         value={this.state.inputValue}
                         autoFocus={this.state.focused}
diff --git a/frontend/src/metabase/pulse/components/SchedulePicker.jsx b/frontend/src/metabase/pulse/components/SchedulePicker.jsx
index 7e1df516236f86ad69dcff9f451edb25a50d75c0..99e70181c37960d4ed5a2a5839b88653f4bcd024 100644
--- a/frontend/src/metabase/pulse/components/SchedulePicker.jsx
+++ b/frontend/src/metabase/pulse/components/SchedulePicker.jsx
@@ -56,6 +56,7 @@ export default class SchedulePicker extends Component {
                     value={_.find(MONTH_DAY_OPTIONS, (o) => o.value === c.schedule_frame)}
                     options={MONTH_DAY_OPTIONS}
                     optionNameFn={o => o.name}
+                    className="bg-white"
                     optionValueFn={o => o.value}
                     onChange={(o) => this.props.onPropertyChange("schedule_frame", o) }
                 />
@@ -66,6 +67,7 @@ export default class SchedulePicker extends Component {
                             options={DAY_OPTIONS}
                             optionNameFn={o => o.name}
                             optionValueFn={o => o.value}
+                            className="bg-white"
                             onChange={(o) => this.props.onPropertyChange("schedule_day", o) }
                         />
                     </span>
@@ -83,6 +85,7 @@ export default class SchedulePicker extends Component {
                     options={DAY_OF_WEEK_OPTIONS}
                     optionNameFn={o => o.name}
                     optionValueFn={o => o.value}
+                    className="bg-white"
                     onChange={(o) => this.props.onPropertyChange("schedule_day", o) }
                 />
             </span>
diff --git a/frontend/src/metabase/qb/.eslintrc b/frontend/src/metabase/qb/.eslintrc
new file mode 100644
index 0000000000000000000000000000000000000000..b838b6ce387bf8a92740f01a96971b63b99d3eff
--- /dev/null
+++ b/frontend/src/metabase/qb/.eslintrc
@@ -0,0 +1,5 @@
+{
+  "rules": {
+    "flowtype/require-valid-file-annotation": [2, "always"]
+  }
+}
diff --git a/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx b/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..b8c6348ff9f1e3daa44f1a999de69d9b5eac16d2
--- /dev/null
+++ b/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx
@@ -0,0 +1,162 @@
+/* @flow weak */
+
+import React, { Component, PropTypes } from "react";
+
+import DatePicker
+    from "metabase/query_builder/components/filters/pickers/DatePicker";
+import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
+import { SelectButton } from "metabase/components/Select";
+import Button from "metabase/components/Button";
+
+import * as Query from "metabase/lib/query/query";
+import * as Filter from "metabase/lib/query/filter";
+import * as Field from "metabase/lib/query/field";
+import * as Card from "metabase/meta/Card";
+
+import {
+    parseFieldTarget,
+    parseFieldTargetId,
+    generateTimeFilterValuesDescriptions
+} from "metabase/lib/query_time";
+
+import cx from "classnames";
+import _ from "underscore";
+
+import type {
+    Card as CardObject,
+    DatasetQuery
+} from "metabase/meta/types/Card";
+import type { TableMetadata } from "metabase/meta/types/Metadata";
+import type { FieldFilter } from "metabase/meta/types/Query";
+
+type Props = {
+    className: string,
+    card: CardObject,
+    tableMetadata: TableMetadata,
+    setDatasetQuery: (datasetQuery: DatasetQuery) => void,
+    runQuery: () => void
+};
+
+type State = {
+    filterIndex: number,
+    filter: FieldFilter,
+    currentFilter: any
+};
+
+export default class TimeseriesFilterWidget extends Component<*, Props, State> {
+    state = {
+        filter: null,
+        filterIndex: -1,
+        currentFilter: null
+    };
+
+    _popover: ?any;
+
+    componentWillMount() {
+        this.componentWillReceiveProps(this.props);
+    }
+
+    componentWillReceiveProps(nextProps: Props) {
+        const query = Card.getQuery(nextProps.card);
+        if (query) {
+            const breakouts = Query.getBreakouts(query);
+            const filters = Query.getFilters(query);
+
+            const timeFieldId = parseFieldTargetId(breakouts[0]);
+            const timeField = parseFieldTarget(breakouts[0]);
+
+            const filterIndex = _.findIndex(
+                filters,
+                filter =>
+                    Filter.isFieldFilter(filter) &&
+                    Field.getFieldTargetId(filter[1]) === timeFieldId
+            );
+
+            let filter, currentFilter;
+            if (filterIndex >= 0) {
+                filter = (currentFilter = filters[filterIndex]);
+            } else {
+                filter = ["time-interval", timeField, -30, "day"];
+            }
+
+            // $FlowFixMe
+            this.setState({ filter, filterIndex, currentFilter });
+        }
+    }
+
+    render() {
+        const {
+            className,
+            card,
+            tableMetadata,
+            setDatasetQuery,
+            runQuery
+        } = this.props;
+        const { filter, filterIndex, currentFilter } = this.state;
+        let currentDescription;
+
+        if (currentFilter) {
+            currentDescription = generateTimeFilterValuesDescriptions(
+                currentFilter
+            ).join(" - ");
+            if (currentFilter[0] === ">") {
+                currentDescription = "After " + currentDescription;
+            } else if (currentFilter[0] === "<") {
+                currentDescription = "Before " + currentDescription;
+            }
+        } else {
+            currentDescription = "All Time";
+        }
+
+        return (
+            <PopoverWithTrigger
+                triggerElement={
+                    <SelectButton hasValue>
+                        {currentDescription}
+                    </SelectButton>
+                }
+                triggerClasses={cx(className, "my2")}
+                ref={ref => this._popover = ref}
+                sizeToFit
+            >
+                <DatePicker
+                    className="mt2"
+                    filter={this.state.filter}
+                    onFilterChange={newFilter => {
+                        this.setState({ filter: newFilter });
+                    }}
+                    tableMetadata={tableMetadata}
+                />
+                <div className="p1">
+                    <Button
+                        purple
+                        className="full"
+                        onClick={() => {
+                            let query = Card.getQuery(card);
+                            if (query) {
+                                if (filterIndex >= 0) {
+                                    query = Query.updateFilter(
+                                        query,
+                                        filterIndex,
+                                        filter
+                                    );
+                                } else {
+                                    query = Query.addFilter(query, filter);
+                                }
+                                // $FlowFixMe
+                                setDatasetQuery({
+                                    ...card.dataset_query,
+                                    query
+                                });
+                                runQuery();
+                            }
+                            if (this._popover) {
+                                this._popover.close();
+                            }
+                        }}
+                    >Apply</Button>
+                </div>
+            </PopoverWithTrigger>
+        );
+    }
+}
diff --git a/frontend/src/metabase/qb/components/TimeseriesGroupingWidget.jsx b/frontend/src/metabase/qb/components/TimeseriesGroupingWidget.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..b07282f3b549e225b8b44c0d193e008b69fd5f5e
--- /dev/null
+++ b/frontend/src/metabase/qb/components/TimeseriesGroupingWidget.jsx
@@ -0,0 +1,88 @@
+/* @flow weak */
+
+import React, { Component, PropTypes } from "react";
+
+import TimeGroupingPopover
+    from "metabase/query_builder/components/TimeGroupingPopover";
+import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
+import { SelectButton } from "metabase/components/Select";
+
+import * as Query from "metabase/lib/query/query";
+import * as Card from "metabase/meta/Card";
+
+import { parseFieldBucketing, formatBucketing } from "metabase/lib/query_time";
+
+import type {
+    Card as CardObject,
+    DatasetQuery
+} from "metabase/meta/types/Card";
+
+type Props = {
+    card: CardObject,
+    setDatasetQuery: (datasetQuery: DatasetQuery) => void,
+    runQuery: () => void
+};
+
+export default class TimeseriesGroupingWidget extends Component<*, Props, *> {
+    _popover: ?any;
+
+    render() {
+        const { card, setDatasetQuery, runQuery } = this.props;
+        if (Card.isStructured(card)) {
+            const query = Card.getQuery(card);
+            const breakouts = query && Query.getBreakouts(query);
+
+            if (!breakouts || breakouts.length === 0) {
+                return null;
+            }
+
+            return (
+                <PopoverWithTrigger
+                    triggerElement={
+                        <SelectButton hasValue>
+                            {formatBucketing(parseFieldBucketing(breakouts[0]))}
+                        </SelectButton>
+                    }
+                    triggerClasses="my2"
+                    ref={ref => this._popover = ref}
+                >
+                    <TimeGroupingPopover
+                        className="text-brand"
+                        field={breakouts[0]}
+                        onFieldChange={breakout => {
+                            let query = Card.getQuery(card);
+                            if (query) {
+                                query = Query.updateBreakout(
+                                    query,
+                                    0,
+                                    breakout
+                                );
+                                // $FlowFixMe
+                                setDatasetQuery({
+                                    ...card.dataset_query,
+                                    query
+                                });
+                                runQuery();
+                                if (this._popover) {
+                                    this._popover.close();
+                                }
+                            }
+                        }}
+                        title={null}
+                        groupingOptions={[
+                            "minute",
+                            "hour",
+                            "day",
+                            "week",
+                            "month",
+                            "quarter",
+                            "year"
+                        ]}
+                    />
+                </PopoverWithTrigger>
+            );
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/frontend/src/metabase/qb/components/actions/PivotByAction.jsx b/frontend/src/metabase/qb/components/actions/PivotByAction.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..7a1b560f968e7f2824cd326e9f12cd231663e5e1
--- /dev/null
+++ b/frontend/src/metabase/qb/components/actions/PivotByAction.jsx
@@ -0,0 +1,90 @@
+/* @flow */
+
+import React from "react";
+
+import BreakoutPopover from "metabase/qb/components/gui/BreakoutPopover";
+
+import * as Card from "metabase/meta/Card";
+import Query from "metabase/lib/query";
+import { pivot } from "metabase/qb/lib/actions";
+
+import type { Field } from "metabase/meta/types/Field";
+import type {
+    ClickAction,
+    ClickActionProps,
+    ClickActionPopoverProps
+} from "metabase/meta/types/Visualization";
+
+type FieldFilter = (field: Field) => boolean;
+
+// PivotByAction displays a breakout picker, and optionally filters by the
+// clicked dimesion values (and removes corresponding breakouts)
+export default (name: string, icon: string, fieldFilter: FieldFilter) => (
+    { card, tableMetadata, clicked }: ClickActionProps
+): ?ClickAction => {
+    const query = Card.getQuery(card);
+
+    // Click target types: metric value
+    if (
+        !query ||
+        !tableMetadata ||
+        (clicked &&
+            (clicked.value === undefined ||
+                clicked.column.source !== "aggregation"))
+    ) {
+        return;
+    }
+
+    let dimensions = (clicked && clicked.dimensions) || [];
+
+    const breakouts = Query.getBreakouts(query);
+
+    const usedFields = {};
+    for (const breakout of breakouts) {
+        usedFields[Query.getFieldTargetId(breakout)] = true;
+    }
+
+    const fieldOptions = Query.getFieldOptions(
+        tableMetadata.fields,
+        true,
+        (fields: Field[]): Field[] => {
+            fields = tableMetadata.breakout_options.validFieldsFilter(fields);
+            if (fieldFilter) {
+                fields = fields.filter(fieldFilter);
+            }
+            return fields;
+        },
+        usedFields
+    );
+
+    const customFieldOptions = Query.getExpressions(query);
+
+    if (fieldOptions.count === 0) {
+        return null;
+    }
+
+    return {
+        title: (
+            <span>
+                Pivot by
+                {" "}
+                <span className="text-dark">{name.toLowerCase()}</span>
+            </span>
+        ),
+        icon: icon,
+        // eslint-disable-next-line react/display-name
+        popover: ({ onChangeCardAndRun, onClose }: ClickActionPopoverProps) => (
+            <BreakoutPopover
+                tableMetadata={tableMetadata}
+                fieldOptions={fieldOptions}
+                customFieldOptions={customFieldOptions}
+                onCommitBreakout={breakout => {
+                    onChangeCardAndRun(
+                        pivot(card, breakout, tableMetadata, dimensions)
+                    );
+                }}
+                onClose={onClose}
+            />
+        )
+    };
+};
diff --git a/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx b/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..1165f67ffc6972bb4a4f4d8658fa29c53b6d2e2b
--- /dev/null
+++ b/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx
@@ -0,0 +1,13 @@
+/* @flow */
+
+import React from "react";
+
+import { isCategory, isAddress } from "metabase/lib/schema_metadata";
+
+import PivotByAction from "./PivotByAction";
+
+export default PivotByAction(
+    "Category",
+    "label",
+    field => isCategory(field) && !isAddress(field)
+);
diff --git a/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx b/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..218ee7f08200ec2cf15853ae6efc9d7bb36b9ddf
--- /dev/null
+++ b/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx
@@ -0,0 +1,9 @@
+/* @flow */
+
+import React from "react";
+
+import { isAddress } from "metabase/lib/schema_metadata";
+
+import PivotByAction from "./PivotByAction";
+
+export default PivotByAction("Location", "location", field => isAddress(field));
diff --git a/frontend/src/metabase/qb/components/actions/PivotByTimeAction.jsx b/frontend/src/metabase/qb/components/actions/PivotByTimeAction.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..b80c6bdc549b020ae6bfc10110594c1e0a151966
--- /dev/null
+++ b/frontend/src/metabase/qb/components/actions/PivotByTimeAction.jsx
@@ -0,0 +1,9 @@
+/* @flow */
+
+import React from "react";
+
+import { isDate } from "metabase/lib/schema_metadata";
+
+import PivotByAction from "./PivotByAction";
+
+export default PivotByAction("Time", "clock", field => isDate(field));
diff --git a/frontend/src/metabase/qb/components/actions/PlotSegmentField.jsx b/frontend/src/metabase/qb/components/actions/PlotSegmentField.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..29ab9ce34a59a78286cdf964c2a772f9c5ed8a70
--- /dev/null
+++ b/frontend/src/metabase/qb/components/actions/PlotSegmentField.jsx
@@ -0,0 +1,19 @@
+/* @flow */
+
+import { plotSegmentField } from "metabase/qb/lib/actions";
+
+import type {
+    ClickAction,
+    ClickActionProps
+} from "metabase/meta/types/Visualization";
+
+export default ({ card, tableMetadata }: ClickActionProps): ?ClickAction => {
+    if (card.display !== "table") {
+        return;
+    }
+    return {
+        title: "Plot a field in this segment",
+        icon: "bar",
+        card: () => plotSegmentField(card)
+    };
+};
diff --git a/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx b/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..8d05d6502b7926dc22ebae4c412c6d8e6f7d08b9
--- /dev/null
+++ b/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx
@@ -0,0 +1,41 @@
+/* @flow */
+
+import React, { Component, PropTypes } from "react";
+
+import AggregationPopover from "metabase/qb/components/gui/AggregationPopover";
+
+import * as Card from "metabase/meta/Card";
+import Query from "metabase/lib/query";
+import { summarize } from "metabase/qb/lib/actions";
+
+import type {
+    ClickAction,
+    ClickActionProps,
+    ClickActionPopoverProps
+} from "metabase/meta/types/Visualization";
+
+export default ({ card, tableMetadata }: ClickActionProps): ?ClickAction => {
+    const query = Card.getQuery(card);
+    if (!query) {
+        return;
+    }
+
+    return {
+        title: "Summarize this segment",
+        icon: "funnel", // FIXME: icon
+        // eslint-disable-next-line react/display-name
+        popover: ({ onChangeCardAndRun, onClose }: ClickActionPopoverProps) => (
+            <AggregationPopover
+                tableMetadata={tableMetadata}
+                customFields={Query.getExpressions(query)}
+                availableAggregations={tableMetadata.aggregation_options}
+                onCommitAggregation={aggregation => {
+                    onChangeCardAndRun(
+                        summarize(card, aggregation, tableMetadata)
+                    );
+                    onClose && onClose();
+                }}
+            />
+        )
+    };
+};
diff --git a/frontend/src/metabase/qb/components/actions/UnderlyingDataAction.jsx b/frontend/src/metabase/qb/components/actions/UnderlyingDataAction.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..a45bc79fd1b34e12b8ce02ab4659cfbb24772bd5
--- /dev/null
+++ b/frontend/src/metabase/qb/components/actions/UnderlyingDataAction.jsx
@@ -0,0 +1,15 @@
+/* @flow */
+
+import { toUnderlyingData } from "metabase/qb/lib/actions";
+
+import type { ClickActionProps } from "metabase/meta/types/Visualization";
+
+export default ({ card, tableMetadata }: ClickActionProps) => {
+    if (card.display !== "table" && card.display !== "scalar") {
+        return {
+            title: "View the underlying data",
+            icon: "table",
+            card: () => toUnderlyingData(card)
+        };
+    }
+};
diff --git a/frontend/src/metabase/qb/components/actions/UnderlyingRecordsAction.jsx b/frontend/src/metabase/qb/components/actions/UnderlyingRecordsAction.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..d1f2fa34527dca25e47ef386399b8dd70327e292
--- /dev/null
+++ b/frontend/src/metabase/qb/components/actions/UnderlyingRecordsAction.jsx
@@ -0,0 +1,33 @@
+/* @flow */
+
+import React from "react";
+
+import { toUnderlyingRecords } from "metabase/qb/lib/actions";
+import * as Query from "metabase/lib/query/query";
+import * as Card from "metabase/meta/Card";
+
+import type { ClickActionProps } from "metabase/meta/types/Visualization";
+
+export default ({ card, tableMetadata }: ClickActionProps) => {
+    const query = Card.getQuery(card);
+    if (!query) {
+        return;
+    }
+    if (!Query.isBareRows(query)) {
+        return {
+            title: (
+                <span>
+                    View the underlying
+                    {" "}
+                    <span className="text-dark">
+                        {tableMetadata.display_name}
+                    </span>
+                    {" "}
+                    records
+                </span>
+            ),
+            icon: "table",
+            card: () => toUnderlyingRecords(card)
+        };
+    }
+};
diff --git a/frontend/src/metabase/qb/components/actions/index.js b/frontend/src/metabase/qb/components/actions/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..bfbda41978d864fec0e45d3d4ac9cc32fc7baa8c
--- /dev/null
+++ b/frontend/src/metabase/qb/components/actions/index.js
@@ -0,0 +1,6 @@
+/* @flow */
+
+import UnderlyingDataAction from "./UnderlyingDataAction";
+import UnderlyingRecordsAction from "./UnderlyingRecordsAction";
+
+export const DEFAULT_ACTIONS = [UnderlyingDataAction, UnderlyingRecordsAction];
diff --git a/frontend/src/metabase/qb/components/drill/ObjectDetailDrill.jsx b/frontend/src/metabase/qb/components/drill/ObjectDetailDrill.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..0ffac3389573283a7626a904a0b58925f08fc6ec
--- /dev/null
+++ b/frontend/src/metabase/qb/components/drill/ObjectDetailDrill.jsx
@@ -0,0 +1,58 @@
+/* @flow */
+
+import React from "react";
+
+import { drillRecord } from "metabase/qb/lib/actions";
+
+import { isFK, isPK } from "metabase/lib/types";
+import { singularize, stripId } from "metabase/lib/formatting";
+
+import * as Table from "metabase/lib/query/table";
+
+import type {
+    ClickAction,
+    ClickActionProps
+} from "metabase/meta/types/Visualization";
+
+export default (
+    { card, tableMetadata, clicked }: ClickActionProps
+): ?ClickAction => {
+    if (
+        !clicked ||
+        !clicked.column ||
+        clicked.value === undefined ||
+        !(isFK(clicked.column.special_type) ||
+            isPK(clicked.column.special_type))
+    ) {
+        return;
+    }
+
+    const value = clicked.value;
+
+    let field = Table.getField(tableMetadata, clicked.column.id);
+    let table = tableMetadata;
+    let recordType = tableMetadata.display_name;
+    if (field.target) {
+        recordType = field.display_name;
+        table = field.target.table;
+        field = field.target;
+    }
+
+    if (!field || !table) {
+        return;
+    }
+
+    return {
+        title: (
+            <span>
+                View this
+                {" "}
+                <span className="text-dark">
+                    {singularize(stripId(recordType))}
+                </span>
+            </span>
+        ),
+        default: true,
+        card: () => drillRecord(tableMetadata.db_id, table.id, field.id, value)
+    };
+};
diff --git a/frontend/src/metabase/qb/components/drill/PivotByCategoryDrill.jsx b/frontend/src/metabase/qb/components/drill/PivotByCategoryDrill.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..f10a4d8aa883711cd2835519fce48fa98f2feb5b
--- /dev/null
+++ b/frontend/src/metabase/qb/components/drill/PivotByCategoryDrill.jsx
@@ -0,0 +1,16 @@
+/* @flow */
+
+import React, { Component, PropTypes } from "react";
+
+import PivotByCategoryAction from "../actions/PivotByCategoryAction";
+
+import type {
+    ClickAction,
+    ClickActionProps
+} from "metabase/meta/types/Visualization";
+
+export default (
+    { card, tableMetadata, clicked }: ClickActionProps
+): ?ClickAction => {
+    return PivotByCategoryAction({ card, tableMetadata, clicked });
+};
diff --git a/frontend/src/metabase/qb/components/drill/PivotByLocationDrill.jsx b/frontend/src/metabase/qb/components/drill/PivotByLocationDrill.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..385a54969550c2c2a75eb29c69c5d10f7e8872fb
--- /dev/null
+++ b/frontend/src/metabase/qb/components/drill/PivotByLocationDrill.jsx
@@ -0,0 +1,16 @@
+/* @flow */
+
+import React, { Component, PropTypes } from "react";
+
+import PivotByLocationAction from "../actions/PivotByLocationAction";
+
+import type {
+    ClickAction,
+    ClickActionProps
+} from "metabase/meta/types/Visualization";
+
+export default (
+    { card, tableMetadata, clicked }: ClickActionProps
+): ?ClickAction => {
+    return PivotByLocationAction({ card, tableMetadata, clicked });
+};
diff --git a/frontend/src/metabase/qb/components/drill/PivotByTimeDrill.jsx b/frontend/src/metabase/qb/components/drill/PivotByTimeDrill.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..110d5c12336756e9c662a951576c13c3f9636d3c
--- /dev/null
+++ b/frontend/src/metabase/qb/components/drill/PivotByTimeDrill.jsx
@@ -0,0 +1,16 @@
+/* @flow */
+
+import React, { Component, PropTypes } from "react";
+
+import PivotByTimeAction from "../actions/PivotByTimeAction";
+
+import type {
+    ClickAction,
+    ClickActionProps
+} from "metabase/meta/types/Visualization";
+
+export default (
+    { card, tableMetadata, clicked }: ClickActionProps
+): ?ClickAction => {
+    return PivotByTimeAction({ card, tableMetadata, clicked });
+};
diff --git a/frontend/src/metabase/qb/components/drill/QuickFilterDrill.jsx b/frontend/src/metabase/qb/components/drill/QuickFilterDrill.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..e70a957822a696160d6d545935e4e9dbcb0e366c
--- /dev/null
+++ b/frontend/src/metabase/qb/components/drill/QuickFilterDrill.jsx
@@ -0,0 +1,96 @@
+/* @flow */
+
+import React from "react";
+
+import { TYPE, isa, isFK, isPK } from "metabase/lib/types";
+import { singularize, pluralize, stripId } from "metabase/lib/formatting";
+
+import { filter } from "metabase/qb/lib/actions";
+
+import type {
+    ClickAction,
+    ClickActionProps
+} from "metabase/meta/types/Visualization";
+
+function getFiltersForColumn(column) {
+    if (
+        isa(column.base_type, TYPE.Number) ||
+        isa(column.base_type, TYPE.DateTime)
+    ) {
+        return [
+            { name: "<", operator: "<" },
+            { name: "=", operator: "=" },
+            { name: "≠", operator: "!=" },
+            { name: ">", operator: ">" }
+        ];
+    } else {
+        return [{ name: "=", operator: "=" }, { name: "≠", operator: "!=" }];
+    }
+}
+
+export default (
+    { card, tableMetadata, clicked }: ClickActionProps
+): ?ClickAction => {
+    if (
+        !clicked ||
+        !clicked.column ||
+        clicked.column.id == null ||
+        clicked.value == undefined
+    ) {
+        return;
+    }
+
+    const { value, column } = clicked;
+
+    if (isPK(column.special_type)) {
+        return null;
+    } else if (isFK(column.special_type)) {
+        return {
+            title: (
+                <span>
+                    View this
+                    {" "}
+                    {singularize(stripId(column.display_name))}
+                    's
+                    {" "}
+                    {pluralize(tableMetadata.display_name)}
+                </span>
+            ),
+            card: () => filter(card, "=", column, value)
+        };
+    }
+
+    let operators = getFiltersForColumn(column);
+    if (!operators || operators.length === 0) {
+        return;
+    }
+
+    return {
+        title: (
+            <span>
+                Filter by this value
+            </span>
+        ),
+        default: true,
+        popover({ onChangeCardAndRun, onClose }) {
+            return (
+                <ul className="h1 flex align-center px1">
+                    {operators &&
+                        operators.map(({ name, operator }) => (
+                            <li
+                                key={operator}
+                                className="p2 text-brand-hover cursor-pointer"
+                                onClick={() => {
+                                    onChangeCardAndRun(
+                                        filter(card, operator, column, value)
+                                    );
+                                }}
+                            >
+                                {name}
+                            </li>
+                        ))}
+                </ul>
+            );
+        }
+    };
+};
diff --git a/frontend/src/metabase/qb/components/drill/SortAction.jsx b/frontend/src/metabase/qb/components/drill/SortAction.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..e37920c2ac68c365bdccb12489b9a5cdfe9ceb82
--- /dev/null
+++ b/frontend/src/metabase/qb/components/drill/SortAction.jsx
@@ -0,0 +1,73 @@
+/* @flow */
+
+import React from "react";
+
+import { assocIn } from "icepick";
+import Query from "metabase/lib/query";
+import * as Card from "metabase/meta/Card";
+
+import type {
+    ClickAction,
+    ClickActionProps
+} from "metabase/meta/types/Visualization";
+
+export default (
+    { card, tableMetadata, clicked }: ClickActionProps
+): ?ClickAction => {
+    const query = Card.getQuery(card);
+
+    if (
+        !query ||
+        !clicked ||
+        !clicked.column ||
+        clicked.value !== undefined ||
+        !clicked.column.source
+    ) {
+        return;
+    }
+    const { column } = clicked;
+
+    return {
+        title: (
+            <span>
+                Sort by {column.display_name}
+            </span>
+        ),
+        default: true,
+        card: () => {
+            let field = null;
+            if (column.id == null) {
+                // ICK.  this is hacky for dealing with aggregations.  need something better
+                // DOUBLE ICK.  we also need to deal with custom fields now as well
+                const expressions = Query.getExpressions(query);
+                if (column.display_name in expressions) {
+                    field = ["expression", column.display_name];
+                } else {
+                    field = ["aggregation", 0];
+                }
+            } else {
+                field = column.id;
+            }
+
+            let sortClause = [field, "ascending"];
+
+            if (
+                query.order_by &&
+                query.order_by.length > 0 &&
+                query.order_by[0].length > 0 &&
+                query.order_by[0][1] === "ascending" &&
+                Query.isSameField(query.order_by[0][0], field)
+            ) {
+                // someone triggered another sort on the same column, so flip the sort direction
+                sortClause = [field, "descending"];
+            }
+
+            // set clause
+            return assocIn(
+                card,
+                ["dataset_query", "query", "order_by"],
+                [sortClause]
+            );
+        }
+    };
+};
diff --git a/frontend/src/metabase/qb/components/drill/TimeseriesPivotDrill.jsx b/frontend/src/metabase/qb/components/drill/TimeseriesPivotDrill.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..da69baae8738f4eaea433e09007bde20eaf68169
--- /dev/null
+++ b/frontend/src/metabase/qb/components/drill/TimeseriesPivotDrill.jsx
@@ -0,0 +1,33 @@
+/* @flow */
+
+import React from "react";
+
+import { pivot, drillDownForDimensions } from "metabase/qb/lib/actions";
+
+import type {
+    ClickAction,
+    ClickActionProps
+} from "metabase/meta/types/Visualization";
+
+export default (
+    { card, tableMetadata, clicked }: ClickActionProps
+): ?ClickAction => {
+    const dimensions = (clicked && clicked.dimensions) || [];
+    const drilldown = drillDownForDimensions(dimensions);
+    if (!drilldown) {
+        return;
+    }
+
+    return {
+        title: (
+            <span>
+                Drill into this
+                {" "}
+                <span className="text-dark">
+                    {drilldown.name}
+                </span>
+            </span>
+        ),
+        card: () => pivot(card, drilldown.breakout, tableMetadata, dimensions)
+    };
+};
diff --git a/frontend/src/metabase/qb/components/drill/UnderlyingRecordsDrill.jsx b/frontend/src/metabase/qb/components/drill/UnderlyingRecordsDrill.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..1f3c0d8052d73218b2b1d35ade10a2ae7d1a667c
--- /dev/null
+++ b/frontend/src/metabase/qb/components/drill/UnderlyingRecordsDrill.jsx
@@ -0,0 +1,37 @@
+/* @flow */
+
+import React from "react";
+
+import { drillUnderlyingRecords } from "metabase/qb/lib/actions";
+
+import { inflect } from "metabase/lib/formatting";
+
+import type {
+    ClickAction,
+    ClickActionProps
+} from "metabase/meta/types/Visualization";
+
+export default (
+    { card, tableMetadata, clicked }: ClickActionProps
+): ?ClickAction => {
+    const dimensions = (clicked && clicked.dimensions) || [];
+    if (!clicked || dimensions.length === 0) {
+        return;
+    }
+
+    // the metric value should be the number of rows that will be displayed
+    const count = typeof clicked.value === "number" ? clicked.value : 2;
+
+    return {
+        title: (
+            <span>
+                View {inflect("these", count, "this", "these")}
+                {" "}
+                <span className="text-dark">
+                    {inflect(tableMetadata.display_name, count)}
+                </span>
+            </span>
+        ),
+        card: () => drillUnderlyingRecords(card, dimensions)
+    };
+};
diff --git a/frontend/src/metabase/qb/components/drill/index.js b/frontend/src/metabase/qb/components/drill/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..e02e20d305a0b46fc56700c43d0ee162f0609a55
--- /dev/null
+++ b/frontend/src/metabase/qb/components/drill/index.js
@@ -0,0 +1,13 @@
+/* @flow */
+
+import SortAction from "./SortAction";
+import ObjectDetailDrill from "./ObjectDetailDrill";
+import QuickFilterDrill from "./QuickFilterDrill";
+import UnderlyingRecordsDrill from "./UnderlyingRecordsDrill";
+
+export const DEFAULT_DRILLS = [
+    SortAction,
+    ObjectDetailDrill,
+    QuickFilterDrill,
+    UnderlyingRecordsDrill
+];
diff --git a/frontend/src/metabase/qb/components/gui/AggregationPopover.jsx b/frontend/src/metabase/qb/components/gui/AggregationPopover.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..e5ff6ff997038f31d6aaf371033c6f3f9dceded0
--- /dev/null
+++ b/frontend/src/metabase/qb/components/gui/AggregationPopover.jsx
@@ -0,0 +1,22 @@
+/* @flow */
+
+import React, { Component, PropTypes } from "react";
+
+import AggPopover from "metabase/query_builder/components/AggregationPopover";
+
+import type { Aggregation, ExpressionName } from "metabase/meta/types/Query";
+import type { TableMetadata } from "metabase/meta/types/Metadata";
+
+type Props = {
+    aggregation?: Aggregation,
+    tableMetadata: TableMetadata,
+    customFields: { [key: ExpressionName]: any },
+    onCommitAggregation: (aggregation: Aggregation) => void,
+    onClose?: () => void
+};
+
+const AggregationPopover = (props: Props) => (
+    <AggPopover {...props} aggregation={props.aggregation || []} />
+);
+
+export default AggregationPopover;
diff --git a/frontend/src/metabase/qb/components/gui/BreakoutPopover.jsx b/frontend/src/metabase/qb/components/gui/BreakoutPopover.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..285fe299bf3f88eab7707ce725e8c95121da2897
--- /dev/null
+++ b/frontend/src/metabase/qb/components/gui/BreakoutPopover.jsx
@@ -0,0 +1,46 @@
+/* @flow */
+
+import React, { Component, PropTypes } from "react";
+
+import FieldList from "metabase/query_builder/components/FieldList.jsx";
+
+import type { Breakout, ExpressionName } from "metabase/meta/types/Query";
+import type { TableMetadata, FieldOptions } from "metabase/meta/types/Metadata";
+
+type Props = {
+    breakout?: Breakout,
+    tableMetadata: TableMetadata,
+    fieldOptions: FieldOptions,
+    customFieldOptions: { [key: ExpressionName]: any },
+    onCommitBreakout: (breakout: Breakout) => void,
+    onClose?: () => void
+};
+
+const BreakoutPopover = (
+    {
+        breakout,
+        tableMetadata,
+        fieldOptions,
+        customFieldOptions,
+        onCommitBreakout,
+        onClose
+    }: Props
+) => (
+    <FieldList
+        className="text-green"
+        tableMetadata={tableMetadata}
+        field={breakout}
+        fieldOptions={fieldOptions}
+        customFieldOptions={customFieldOptions}
+        onFieldChange={field => {
+            onCommitBreakout(field);
+            if (onClose) {
+                onClose();
+            }
+        }}
+        enableTimeGrouping
+        alwaysExpanded
+    />
+);
+
+export default BreakoutPopover;
diff --git a/frontend/src/metabase/qb/components/modes/DefaultMode.jsx b/frontend/src/metabase/qb/components/modes/DefaultMode.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..6246528a33aa9192a1c6cf93c662489b76faf7f7
--- /dev/null
+++ b/frontend/src/metabase/qb/components/modes/DefaultMode.jsx
@@ -0,0 +1,14 @@
+/* @flow */
+
+import { DEFAULT_ACTIONS } from "../actions";
+import { DEFAULT_DRILLS } from "../drill";
+
+import type { QueryMode } from "metabase/meta/types/Visualization";
+
+const DefaultMode: QueryMode = {
+    name: "default",
+    actions: DEFAULT_ACTIONS,
+    drills: DEFAULT_DRILLS
+};
+
+export default DefaultMode;
diff --git a/frontend/src/metabase/qb/components/modes/GeoMode.jsx b/frontend/src/metabase/qb/components/modes/GeoMode.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..1476a911907dca60d3bbcbd37de027e8a7c8a372
--- /dev/null
+++ b/frontend/src/metabase/qb/components/modes/GeoMode.jsx
@@ -0,0 +1,20 @@
+/* @flow */
+
+import { DEFAULT_ACTIONS } from "../actions";
+import { DEFAULT_DRILLS } from "../drill";
+
+import PivotByCategoryAction from "../actions/PivotByCategoryAction";
+import PivotByTimeAction from "../actions/PivotByTimeAction";
+
+import PivotByCategoryDrill from "../drill/PivotByCategoryDrill";
+import PivotByTimeDrill from "../drill/PivotByTimeDrill";
+
+import type { QueryMode } from "metabase/meta/types/Visualization";
+
+const GeoMode: QueryMode = {
+    name: "geo",
+    actions: [...DEFAULT_ACTIONS, PivotByCategoryAction, PivotByTimeAction],
+    drills: [...DEFAULT_DRILLS, PivotByCategoryDrill, PivotByTimeDrill]
+};
+
+export default GeoMode;
diff --git a/frontend/src/metabase/qb/components/modes/MetricMode.jsx b/frontend/src/metabase/qb/components/modes/MetricMode.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..9a5109308bdf0dfc46174b9c7905bb96d392609b
--- /dev/null
+++ b/frontend/src/metabase/qb/components/modes/MetricMode.jsx
@@ -0,0 +1,32 @@
+/* @flow */
+
+import { DEFAULT_ACTIONS } from "../actions";
+import { DEFAULT_DRILLS } from "../drill";
+
+import PivotByCategoryAction from "../actions/PivotByCategoryAction";
+import PivotByLocationAction from "../actions/PivotByLocationAction";
+import PivotByTimeAction from "../actions/PivotByTimeAction";
+
+import PivotByCategoryDrill from "../drill/PivotByCategoryDrill";
+import PivotByLocationDrill from "../drill/PivotByLocationDrill";
+import PivotByTimeDrill from "../drill/PivotByTimeDrill";
+
+import type { QueryMode } from "metabase/meta/types/Visualization";
+
+const MetricMode: QueryMode = {
+    name: "metric",
+    actions: [
+        ...DEFAULT_ACTIONS,
+        PivotByCategoryAction,
+        PivotByLocationAction,
+        PivotByTimeAction
+    ],
+    drills: [
+        ...DEFAULT_DRILLS,
+        PivotByCategoryDrill,
+        PivotByLocationDrill,
+        PivotByTimeDrill
+    ]
+};
+
+export default MetricMode;
diff --git a/frontend/src/metabase/qb/components/modes/NativeMode.jsx b/frontend/src/metabase/qb/components/modes/NativeMode.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..abaf45d3587774a6bc70c4f867973e8868c17a1f
--- /dev/null
+++ b/frontend/src/metabase/qb/components/modes/NativeMode.jsx
@@ -0,0 +1,13 @@
+/* @flow */
+
+import React from "react";
+
+import type { QueryMode } from "metabase/meta/types/Visualization";
+
+const NativeMode: QueryMode = {
+    name: "native",
+    actions: [],
+    drills: []
+};
+
+export default NativeMode;
diff --git a/frontend/src/metabase/qb/components/modes/PivotMode.jsx b/frontend/src/metabase/qb/components/modes/PivotMode.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..2751893db3d9a16bfc6bc093406cc926106d269b
--- /dev/null
+++ b/frontend/src/metabase/qb/components/modes/PivotMode.jsx
@@ -0,0 +1,32 @@
+/* @flow */
+
+import { DEFAULT_ACTIONS } from "../actions";
+import { DEFAULT_DRILLS } from "../drill";
+
+import PivotByCategoryAction from "../actions/PivotByCategoryAction";
+import PivotByLocationAction from "../actions/PivotByLocationAction";
+import PivotByTimeAction from "../actions/PivotByTimeAction";
+
+import PivotByCategoryDrill from "../drill/PivotByCategoryDrill";
+import PivotByLocationDrill from "../drill/PivotByLocationDrill";
+import PivotByTimeDrill from "../drill/PivotByTimeDrill";
+
+import type { QueryMode } from "metabase/meta/types/Visualization";
+
+const PivotMode: QueryMode = {
+    name: "pivot",
+    actions: [
+        ...DEFAULT_ACTIONS,
+        PivotByCategoryAction,
+        PivotByLocationAction,
+        PivotByTimeAction
+    ],
+    drills: [
+        ...DEFAULT_DRILLS,
+        PivotByCategoryDrill,
+        PivotByLocationDrill,
+        PivotByTimeDrill
+    ]
+};
+
+export default PivotMode;
diff --git a/frontend/src/metabase/qb/components/modes/SegmentMode.jsx b/frontend/src/metabase/qb/components/modes/SegmentMode.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..974154f82bb70f8acefd6f529d8b7ceb0f38bca0
--- /dev/null
+++ b/frontend/src/metabase/qb/components/modes/SegmentMode.jsx
@@ -0,0 +1,25 @@
+/* @flow */
+
+import React from "react";
+
+import { DEFAULT_ACTIONS } from "../actions";
+import { DEFAULT_DRILLS } from "../drill";
+
+import SummarizeBySegmentMetricAction
+    from "../actions/SummarizeBySegmentMetricAction";
+// import PlotSegmentField from "../actions/PlotSegmentField";
+
+import type { QueryMode } from "metabase/meta/types/Visualization";
+
+const SegmentMode: QueryMode = {
+    name: "segment",
+    actions: [
+        ...DEFAULT_ACTIONS,
+        SummarizeBySegmentMetricAction
+        // commenting this out until we sort out viz settings in QB2
+        // PlotSegmentField
+    ],
+    drills: [...DEFAULT_DRILLS]
+};
+
+export default SegmentMode;
diff --git a/frontend/src/metabase/qb/components/modes/TimeseriesMode.jsx b/frontend/src/metabase/qb/components/modes/TimeseriesMode.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..2842d620b655da38b7c16bf7e41c1a82bc8dfab8
--- /dev/null
+++ b/frontend/src/metabase/qb/components/modes/TimeseriesMode.jsx
@@ -0,0 +1,58 @@
+/* @flow */
+
+import React, { Component, PropTypes } from "react";
+
+import TimeseriesGroupingWidget
+    from "metabase/qb/components/TimeseriesGroupingWidget";
+import TimeseriesFilterWidget
+    from "metabase/qb/components/TimeseriesFilterWidget";
+
+import { DEFAULT_ACTIONS } from "../actions";
+import { DEFAULT_DRILLS } from "../drill";
+
+import PivotByCategoryAction from "../actions/PivotByCategoryAction";
+import PivotByLocationAction from "../actions/PivotByLocationAction";
+
+import PivotByCategoryDrill from "../drill/PivotByCategoryDrill";
+import PivotByLocationDrill from "../drill/PivotByLocationDrill";
+
+import TimeseriesPivotDrill from "../drill/TimeseriesPivotDrill";
+
+import type { QueryMode } from "metabase/meta/types/Visualization";
+import type {
+    Card as CardObject,
+    DatasetQuery
+} from "metabase/meta/types/Card";
+import type { TableMetadata } from "metabase/meta/types/Metadata";
+
+type Props = {
+    lastRunCard: CardObject,
+    tableMetadata: TableMetadata,
+    setDatasetQuery: (datasetQuery: DatasetQuery) => void,
+    runQuery: () => void
+};
+
+export const TimeseriesModeFooter = (props: Props) => {
+    return (
+        <div className="flex layout-centered">
+            <span className="mr1">View</span>
+            <TimeseriesFilterWidget {...props} card={props.lastRunCard} />
+            <span className="mx1">by</span>
+            <TimeseriesGroupingWidget {...props} card={props.lastRunCard} />
+        </div>
+    );
+};
+
+const TimeseriesMode: QueryMode = {
+    name: "timeseries",
+    actions: [...DEFAULT_ACTIONS, PivotByCategoryAction, PivotByLocationAction],
+    drills: [
+        ...DEFAULT_DRILLS,
+        TimeseriesPivotDrill,
+        PivotByCategoryDrill,
+        PivotByLocationDrill
+    ],
+    ModeFooter: TimeseriesModeFooter
+};
+
+export default TimeseriesMode;
diff --git a/frontend/src/metabase/qb/lib/actions.js b/frontend/src/metabase/qb/lib/actions.js
new file mode 100644
index 0000000000000000000000000000000000000000..c2667e907383815c6420c017fc44a86c0fde93b1
--- /dev/null
+++ b/frontend/src/metabase/qb/lib/actions.js
@@ -0,0 +1,277 @@
+/* @flow weak */
+
+import moment from "moment";
+
+import Q from "metabase/lib/query"; // legacy query lib
+import * as Card from "metabase/meta/Card";
+import * as Query from "metabase/lib/query/query";
+import * as Field from "metabase/lib/query/field";
+import * as Filter from "metabase/lib/query/filter";
+import { startNewCard } from "metabase/lib/card";
+import { isDate, isState, isCountry } from "metabase/lib/schema_metadata";
+
+import type { Card as CardObject } from "metabase/meta/types/Card";
+import type { TableMetadata } from "metabase/meta/types/Metadata";
+import type { StructuredQuery, FieldFilter } from "metabase/meta/types/Query";
+import type { DimensionValue } from "metabase/meta/types/Visualization";
+
+export const toUnderlyingData = (card: CardObject): ?CardObject => {
+    const newCard = startNewCard("query");
+    newCard.dataset_query = card.dataset_query;
+    newCard.display = "table";
+    return newCard;
+};
+
+export const toUnderlyingRecords = (card: CardObject): ?CardObject => {
+    if (card.dataset_query.type === "query") {
+        const query: StructuredQuery = card.dataset_query.query;
+        const newCard = startNewCard(
+            "query",
+            card.dataset_query.database,
+            query.source_table
+        );
+        newCard.dataset_query.query.filter = query.filter;
+        return newCard;
+    }
+};
+
+export const getFieldClauseFromCol = col => {
+    if (col.fk_field_id != null) {
+        return ["fk->", col.fk_field_id, col.id];
+    } else {
+        return ["field-id", col.id];
+    }
+};
+
+const clone = card => {
+    const newCard = startNewCard("query");
+
+    newCard.display = card.display;
+    newCard.dataset_query = card.dataset_query;
+    newCard.visualization_settings = card.visualization_settings;
+
+    return newCard;
+};
+
+// Adds a new filter with the specified operator, column, and value
+export const filter = (card, operator, column, value) => {
+    const newCard = clone(card);
+    // $FlowFixMe:
+    const filter: FieldFilter = [
+        operator,
+        getFieldClauseFromCol(column),
+        value
+    ];
+    newCard.dataset_query.query = Query.addFilter(
+        newCard.dataset_query.query,
+        filter
+    );
+    return newCard;
+};
+
+const drillFilter = (card, value, column) => {
+    let newCard = clone(card);
+
+    let filter;
+    if (isDate(column)) {
+        filter = [
+            "=",
+            [
+                "datetime-field",
+                getFieldClauseFromCol(column),
+                "as",
+                column.unit
+            ],
+            moment(value).toISOString()
+        ];
+    } else {
+        filter = ["=", getFieldClauseFromCol(column), value];
+    }
+
+    // replace existing filter, if it exists
+    let filters = Query.getFilters(newCard.dataset_query.query);
+    for (let index = 0; index < filters.length; index++) {
+        if (
+            Filter.isFieldFilter(filters[index]) &&
+            Field.getFieldTargetId(filters[index][1]) === column.id
+        ) {
+            newCard.dataset_query.query = Query.updateFilter(
+                newCard.dataset_query.query,
+                index,
+                filter
+            );
+            return newCard;
+        }
+    }
+
+    // otherwise add a new filter
+    newCard.dataset_query.query = Query.addFilter(
+        newCard.dataset_query.query,
+        filter
+    );
+    return newCard;
+};
+
+const UNITS = ["minute", "hour", "day", "week", "month", "quarter", "year"];
+
+export const drillDownForDimensions = dimensions => {
+    const timeDimensions = dimensions.filter(
+        dimension => dimension.column.unit
+    );
+    if (timeDimensions.length === 1) {
+        const column = timeDimensions[0].column;
+        let nextUnit = UNITS[Math.max(0, UNITS.indexOf(column.unit) - 1)];
+        if (nextUnit) {
+            return {
+                name: column.unit,
+                breakout: [
+                    "datetime-field",
+                    getFieldClauseFromCol(column),
+                    "as",
+                    nextUnit
+                ]
+            };
+        }
+    }
+};
+
+export const drillTimeseriesFilter = (card, value, column) => {
+    const newCard = drillFilter(card, value, column);
+
+    let nextUnit = UNITS[Math.max(0, UNITS.indexOf(column.unit) - 1)];
+
+    newCard.dataset_query.query.breakout[0] = [
+        "datetime-field",
+        card.dataset_query.query.breakout[0][1],
+        "as",
+        nextUnit
+    ];
+
+    return newCard;
+};
+
+export const drillUnderlyingRecords = (card, dimensions) => {
+    for (const dimension of dimensions) {
+        card = drillFilter(card, dimension.value, dimension.column);
+    }
+    return toUnderlyingRecords(card);
+};
+
+export const drillRecord = (databaseId, tableId, fieldId, value) => {
+    const newCard = startNewCard("query", databaseId, tableId);
+    newCard.dataset_query.query = Query.addFilter(newCard.dataset_query.query, [
+        "=",
+        fieldId,
+        value
+    ]);
+    return newCard;
+};
+
+export const plotSegmentField = card => {
+    const newCard = startNewCard("query");
+    newCard.display = "scatter";
+    newCard.dataset_query = card.dataset_query;
+    return newCard;
+};
+
+export const summarize = (card, aggregation, tableMetadata) => {
+    const newCard = startNewCard("query");
+    newCard.dataset_query = card.dataset_query;
+    newCard.dataset_query.query = Query.addAggregation(
+        newCard.dataset_query.query,
+        aggregation
+    );
+    guessVisualization(newCard, tableMetadata);
+    return newCard;
+};
+
+export const pivot = (
+    card: CardObject,
+    breakout,
+    tableMetadata: TableMetadata,
+    dimensions: DimensionValue[] = []
+): ?CardObject => {
+    if (card.dataset_query.type !== "query") {
+        return null;
+    }
+
+    let newCard = startNewCard("query");
+    newCard.dataset_query = card.dataset_query;
+
+    for (const dimension of dimensions) {
+        newCard = drillFilter(newCard, dimension.value, dimension.column);
+        const breakoutFields = Query.getBreakoutFields(
+            newCard.dataset_query.query,
+            tableMetadata
+        );
+        for (const [index, field] of breakoutFields.entries()) {
+            if (field && field.id === dimension.column.id) {
+                newCard.dataset_query.query = Query.removeBreakout(
+                    newCard.dataset_query.query,
+                    index
+                );
+            }
+        }
+    }
+
+    newCard.dataset_query.query = Query.addBreakout(
+        // $FlowFixMe: we know newCard is a StructuredDatasetQuery but flow doesn't
+        newCard.dataset_query.query,
+        breakout
+    );
+
+    guessVisualization(newCard, tableMetadata);
+
+    return newCard;
+};
+
+// const VISUALIZATIONS_ONE_BREAKOUTS = new Set([
+//     "bar",
+//     "line",
+//     "area",
+//     "row",
+//     "pie",
+//     "map"
+// ]);
+const VISUALIZATIONS_TWO_BREAKOUTS = new Set(["bar", "line", "area"]);
+
+const guessVisualization = (card: CardObject, tableMetadata: TableMetadata) => {
+    const query = Card.getQuery(card);
+    if (!query) {
+        return;
+    }
+    const aggregations = Query.getAggregations(query);
+    const breakoutFields = Query.getBreakouts(query).map(
+        breakout => (Q.getFieldTarget(breakout, tableMetadata) || {}).field
+    );
+    if (aggregations.length === 0 && breakoutFields.length === 0) {
+        card.display = "table";
+    } else if (aggregations.length === 1 && breakoutFields.length === 0) {
+        card.display = "scalar";
+    } else if (aggregations.length === 1 && breakoutFields.length === 1) {
+        if (isState(breakoutFields[0])) {
+            card.display = "map";
+            card.visualization_settings["map.type"] = "region";
+            card.visualization_settings["map.region"] = "us_states";
+        } else if (isCountry(breakoutFields[0])) {
+            card.display = "map";
+            card.visualization_settings["map.type"] = "region";
+            card.visualization_settings["map.region"] = "world_countries";
+        } else if (isDate(breakoutFields[0])) {
+            card.display = "line";
+        } else {
+            card.display = "bar";
+        }
+    } else if (aggregations.length === 1 && breakoutFields.length === 2) {
+        if (!VISUALIZATIONS_TWO_BREAKOUTS.has(card.display)) {
+            if (isDate(breakoutFields[0])) {
+                card.display = "line";
+            } else {
+                card.display = "bar";
+            }
+        }
+    } else {
+        console.warn("Couldn't guess visualization", card);
+        card.display = "table";
+    }
+};
diff --git a/frontend/src/metabase/qb/lib/modes.js b/frontend/src/metabase/qb/lib/modes.js
new file mode 100644
index 0000000000000000000000000000000000000000..aed8f3f378ccd0a40181d3867d63b6d004905738
--- /dev/null
+++ b/frontend/src/metabase/qb/lib/modes.js
@@ -0,0 +1,110 @@
+/* @flow weak */
+
+import Q from "metabase/lib/query"; // legacy query lib
+import { isDate, isAddress, isCategory } from "metabase/lib/schema_metadata";
+import * as Query from "metabase/lib/query/query";
+import * as Card from "metabase/meta/Card";
+
+import SegmentMode from "../components/modes/SegmentMode";
+import MetricMode from "../components/modes/MetricMode";
+import TimeseriesMode from "../components/modes/TimeseriesMode";
+import GeoMode from "../components/modes/GeoMode";
+import PivotMode from "../components/modes/PivotMode";
+import NativeMode from "../components/modes/NativeMode";
+import DefaultMode from "../components/modes/DefaultMode";
+
+import type { Card as CardObject } from "metabase/meta/types/Card";
+import type { TableMetadata } from "metabase/meta/types/Metadata";
+import type {
+    QueryMode,
+    ClickAction,
+    ClickActionProps,
+    ClickObject
+} from "metabase/meta/types/Visualization";
+
+export function getMode(
+    card: CardObject,
+    tableMetadata: ?TableMetadata
+): ?QueryMode {
+    if (!card) {
+        return null;
+    }
+
+    if (Card.isNative(card)) {
+        return NativeMode;
+    }
+
+    const query = Card.getQuery(card);
+    if (Card.isStructured(card) && query) {
+        if (!tableMetadata) {
+            return null;
+        }
+
+        const aggregations = Query.getAggregations(query);
+        const breakouts = Query.getBreakouts(query);
+
+        if (aggregations.length === 0 && breakouts.length === 0) {
+            return SegmentMode;
+        }
+        if (aggregations.length > 0 && breakouts.length === 0) {
+            return MetricMode;
+        }
+        if (aggregations.length > 0 && breakouts.length > 0) {
+            let breakoutFields = breakouts.map(
+                breakout =>
+                    (Q.getFieldTarget(breakout, tableMetadata) || {}).field
+            );
+            if (
+                (breakoutFields.length === 1 && isDate(breakoutFields[0])) ||
+                (breakoutFields.length === 2 &&
+                    isDate(breakoutFields[0]) &&
+                    isCategory(breakoutFields[1]))
+            ) {
+                return TimeseriesMode;
+            }
+            if (breakoutFields.length === 1 && isAddress(breakoutFields[0])) {
+                return GeoMode;
+            }
+            if (
+                (breakoutFields.length === 1 &&
+                    isCategory(breakoutFields[0])) ||
+                (breakoutFields.length === 2 &&
+                    isCategory(breakoutFields[0]) &&
+                    isCategory(breakoutFields[1]))
+            ) {
+                return PivotMode;
+            }
+        }
+    }
+
+    return DefaultMode;
+}
+
+export const getModeActions = (
+    mode: ?QueryMode,
+    card: ?CardObject,
+    tableMetadata: ?TableMetadata
+): ClickAction[] => {
+    if (mode && card && tableMetadata) {
+        const props: ClickActionProps = { card, tableMetadata };
+        return mode.actions
+            .map(actionCreator => actionCreator(props))
+            .filter(action => action);
+    }
+    return [];
+};
+
+export const getModeDrills = (
+    mode: ?QueryMode,
+    card: ?CardObject,
+    tableMetadata: ?TableMetadata,
+    clicked: ?ClickObject
+): ClickAction[] => {
+    if (mode && card && tableMetadata && clicked) {
+        const props: ClickActionProps = { card, tableMetadata, clicked };
+        return mode.drills
+            .map(actionCreator => actionCreator(props))
+            .filter(action => action);
+    }
+    return [];
+};
diff --git a/frontend/src/metabase/query_builder/actions.js b/frontend/src/metabase/query_builder/actions.js
index d38c2f43001c4e044333d531da5137afad6170a0..4baf7bc67caf8d82235f288e7c414acf955ba8cb 100644
--- a/frontend/src/metabase/query_builder/actions.js
+++ b/frontend/src/metabase/query_builder/actions.js
@@ -20,7 +20,7 @@ import { getEngineNativeType, formatJsonQuery } from "metabase/lib/engine";
 import { defer } from "metabase/lib/promise";
 import { applyParameters } from "metabase/meta/Card";
 
-import { isDirty, getParameters, getNativeDatabases } from "./selectors";
+import { getParameters, getNativeDatabases } from "./selectors";
 
 import { MetabaseApi, CardApi, UserApi } from "metabase/services";
 
@@ -112,7 +112,7 @@ export const updateUrl = createThunkAction(UPDATE_URL, (card, { dirty = false, r
 export const RESET_QB = "metabase/qb/RESET_QB";
 export const resetQB = createAction(RESET_QB);
 
-export const INITIALIZE_QB = "INITIALIZE_QB";
+export const INITIALIZE_QB = "metabase/qb/INITIALIZE_QB";
 export const initializeQB = createThunkAction(INITIALIZE_QB, (location, params) => {
     return async (dispatch, getState) => {
         // do this immediately to ensure old state is cleared before the user sees it
@@ -223,7 +223,7 @@ export const initializeQB = createThunkAction(INITIALIZE_QB, (location, params)
         if (card && card.dataset_query && (Query.canRun(card.dataset_query.query) || card.dataset_query.type === "native")) {
             // NOTE: timeout to allow Parameters widget to set parameterValues
             setTimeout(() =>
-                dispatch(runQuery(card, false))
+                dispatch(runQuery(card, { shouldUpdateUrl: false }))
             , 0);
         }
 
@@ -243,22 +243,22 @@ export const initializeQB = createThunkAction(INITIALIZE_QB, (location, params)
 });
 
 
-export const TOGGLE_DATA_REFERENCE = "TOGGLE_DATA_REFERENCE";
+export const TOGGLE_DATA_REFERENCE = "metabase/qb/TOGGLE_DATA_REFERENCE";
 export const toggleDataReference = createAction(TOGGLE_DATA_REFERENCE, () => {
     MetabaseAnalytics.trackEvent("QueryBuilder", "Toggle Data Reference");
 });
 
-export const TOGGLE_TEMPLATE_TAGS_EDITOR = "TOGGLE_TEMPLATE_TAGS_EDITOR";
+export const TOGGLE_TEMPLATE_TAGS_EDITOR = "metabase/qb/TOGGLE_TEMPLATE_TAGS_EDITOR";
 export const toggleTemplateTagsEditor = createAction(TOGGLE_TEMPLATE_TAGS_EDITOR, () => {
     MetabaseAnalytics.trackEvent("QueryBuilder", "Toggle Template Tags Editor");
 });
 
-export const CLOSE_QB_TUTORIAL = "CLOSE_QB_TUTORIAL";
+export const CLOSE_QB_TUTORIAL = "metabase/qb/CLOSE_QB_TUTORIAL";
 export const closeQbTutorial = createAction(CLOSE_QB_TUTORIAL, () => {
     MetabaseAnalytics.trackEvent("QueryBuilder", "Tutorial Close");
 });
 
-export const CLOSE_QB_NEWB_MODAL = "CLOSE_QB_NEWB_MODAL";
+export const CLOSE_QB_NEWB_MODAL = "metabase/qb/CLOSE_QB_NEWB_MODAL";
 export const closeQbNewbModal = createThunkAction(CLOSE_QB_NEWB_MODAL, () => {
     return async (dispatch, getState) => {
         // persist the fact that this user has seen the NewbModal
@@ -269,12 +269,12 @@ export const closeQbNewbModal = createThunkAction(CLOSE_QB_NEWB_MODAL, () => {
 });
 
 
-export const BEGIN_EDITING = "BEGIN_EDITING";
+export const BEGIN_EDITING = "metabase/qb/BEGIN_EDITING";
 export const beginEditing = createAction(BEGIN_EDITING, () => {
     MetabaseAnalytics.trackEvent("QueryBuilder", "Edit Begin");
 });
 
-export const CANCEL_EDITING = "CANCEL_EDITING";
+export const CANCEL_EDITING = "metabase/qb/CANCEL_EDITING";
 export const cancelEditing = createThunkAction(CANCEL_EDITING, () => {
     return (dispatch, getState) => {
         const { qb: { originalCard } } = getState();
@@ -285,7 +285,7 @@ export const cancelEditing = createThunkAction(CANCEL_EDITING, () => {
         dispatch(loadMetadataForCard(card));
 
         // we do this to force the indication of the fact that the card should not be considered dirty when the url is updated
-        dispatch(runQuery(card, false));
+        dispatch(runQuery(card, { shouldUpdateUrl: false }));
         dispatch(updateUrl(card, { dirty: false }));
 
         MetabaseAnalytics.trackEvent("QueryBuilder", "Edit Cancel");
@@ -293,7 +293,7 @@ export const cancelEditing = createThunkAction(CANCEL_EDITING, () => {
     };
 });
 
-export const LOAD_METADATA_FOR_CARD = "LOAD_METADATA_FOR_CARD";
+export const LOAD_METADATA_FOR_CARD = "metabase/qb/LOAD_METADATA_FOR_CARD";
 export const loadMetadataForCard = createThunkAction(LOAD_METADATA_FOR_CARD, (card) => {
     return async (dispatch, getState) => {
         // if we have a card with a known source table then dispatch an action to load up that info
@@ -307,7 +307,7 @@ export const loadMetadataForCard = createThunkAction(LOAD_METADATA_FOR_CARD, (ca
     }
 });
 
-export const LOAD_TABLE_METADATA = "LOAD_TABLE_METADATA";
+export const LOAD_TABLE_METADATA = "metabase/qb/LOAD_TABLE_METADATA";
 export const loadTableMetadata = createThunkAction(LOAD_TABLE_METADATA, (tableId) => {
     return async (dispatch, getState) => {
         // if we already have the metadata loaded for the given table then we are done
@@ -325,7 +325,7 @@ export const loadTableMetadata = createThunkAction(LOAD_TABLE_METADATA, (tableId
     };
 });
 
-export const LOAD_DATABASE_FIELDS = "LOAD_DATABASE_FIELDS";
+export const LOAD_DATABASE_FIELDS = "metabase/qb/LOAD_DATABASE_FIELDS";
 export const loadDatabaseFields = createThunkAction(LOAD_DATABASE_FIELDS, (dbId) => {
     return async (dispatch, getState) => {
         // if we already have the metadata loaded for the given table then we are done
@@ -350,6 +350,14 @@ export const loadDatabaseFields = createThunkAction(LOAD_DATABASE_FIELDS, (dbId)
 });
 
 function updateVisualizationSettings(card, isEditing, display, vizSettings) {
+    // don't need to store undefined
+    vizSettings = Utils.copy(vizSettings)
+    for (const name in vizSettings) {
+        if (vizSettings[name] === undefined) {
+            delete vizSettings[name];
+        }
+    }
+
     // make sure that something actually changed
     if (card.display === display && _.isEqual(card.visualization_settings, vizSettings)) return card;
 
@@ -368,10 +376,10 @@ function updateVisualizationSettings(card, isEditing, display, vizSettings) {
     return updatedCard;
 }
 
-export const SET_CARD_ATTRIBUTE = "SET_CARD_ATTRIBUTE";
+export const SET_CARD_ATTRIBUTE = "metabase/qb/SET_CARD_ATTRIBUTE";
 export const setCardAttribute = createAction(SET_CARD_ATTRIBUTE, (attr, value) => ({attr, value}));
 
-export const SET_CARD_VISUALIZATION = "SET_CARD_VISUALIZATION";
+export const SET_CARD_VISUALIZATION = "metabase/qb/SET_CARD_VISUALIZATION";
 export const setCardVisualization = createThunkAction(SET_CARD_VISUALIZATION, (display) => {
     return (dispatch, getState) => {
         const { qb: { card, uiControls } } = getState();
@@ -381,7 +389,7 @@ export const setCardVisualization = createThunkAction(SET_CARD_VISUALIZATION, (d
     }
 });
 
-export const UPDATE_CARD_VISUALIZATION_SETTINGS = "UPDATE_CARD_VISUALIZATION_SETTINGS";
+export const UPDATE_CARD_VISUALIZATION_SETTINGS = "metabase/qb/UPDATE_CARD_VISUALIZATION_SETTINGS";
 export const updateCardVisualizationSettings = createThunkAction(UPDATE_CARD_VISUALIZATION_SETTINGS, (settings) => {
     return (dispatch, getState) => {
         const { qb: { card, uiControls } } = getState();
@@ -391,7 +399,7 @@ export const updateCardVisualizationSettings = createThunkAction(UPDATE_CARD_VIS
     };
 });
 
-export const REPLACE_ALL_CARD_VISUALIZATION_SETTINGS = "REPLACE_ALL_CARD_VISUALIZATION_SETTINGS";
+export const REPLACE_ALL_CARD_VISUALIZATION_SETTINGS = "metabase/qb/REPLACE_ALL_CARD_VISUALIZATION_SETTINGS";
 export const replaceAllCardVisualizationSettings = createThunkAction(REPLACE_ALL_CARD_VISUALIZATION_SETTINGS, (settings) => {
     return (dispatch, getState) => {
         const { qb: { card, uiControls } } = getState();
@@ -401,7 +409,7 @@ export const replaceAllCardVisualizationSettings = createThunkAction(REPLACE_ALL
     };
 });
 
-export const UPDATE_TEMPLATE_TAG = "UPDATE_TEMPLATE_TAG";
+export const UPDATE_TEMPLATE_TAG = "metabase/qb/UPDATE_TEMPLATE_TAG";
 export const updateTemplateTag = createThunkAction(UPDATE_TEMPLATE_TAG, (templateTag) => {
     return (dispatch, getState) => {
         const { qb: { card, uiControls } } = getState();
@@ -419,12 +427,12 @@ export const updateTemplateTag = createThunkAction(UPDATE_TEMPLATE_TAG, (templat
     };
 });
 
-export const SET_PARAMETER_VALUE = "SET_PARAMETER_VALUE";
+export const SET_PARAMETER_VALUE = "metabase/qb/SET_PARAMETER_VALUE";
 export const setParameterValue = createAction(SET_PARAMETER_VALUE, (parameterId, value) => {
     return { id: parameterId, value };
 });
 
-export const NOTIFY_CARD_CREATED = "NOTIFY_CARD_CREATED";
+export const NOTIFY_CARD_CREATED = "metabase/qb/NOTIFY_CARD_CREATED";
 export const notifyCardCreatedFn = createThunkAction(NOTIFY_CARD_CREATED, (card) => {
     return (dispatch, getState) => {
         dispatch(updateUrl(card, { dirty: false }));
@@ -435,7 +443,7 @@ export const notifyCardCreatedFn = createThunkAction(NOTIFY_CARD_CREATED, (card)
     }
 });
 
-export const NOTIFY_CARD_UPDATED = "NOTIFY_CARD_UPDATED";
+export const NOTIFY_CARD_UPDATED = "metabase/qb/NOTIFY_CARD_UPDATED";
 export const notifyCardUpdatedFn = createThunkAction("NOTIFY_CARD_UPDATED", (card) => {
     return (dispatch, getState) => {
         dispatch(updateUrl(card, { dirty: false }));
@@ -447,7 +455,7 @@ export const notifyCardUpdatedFn = createThunkAction("NOTIFY_CARD_UPDATED", (car
 });
 
 // reloadCard
-export const RELOAD_CARD = "RELOAD_CARD";
+export const RELOAD_CARD = "metabase/qb/RELOAD_CARD";
 export const reloadCard = createThunkAction(RELOAD_CARD, () => {
     return async (dispatch, getState) => {
         const { qb: { originalCard } } = getState();
@@ -458,7 +466,7 @@ export const reloadCard = createThunkAction(RELOAD_CARD, () => {
         dispatch(loadMetadataForCard(card));
 
         // we do this to force the indication of the fact that the card should not be considered dirty when the url is updated
-        dispatch(runQuery(card, false));
+        dispatch(runQuery(card, { shouldUpdateUrl: false }));
         dispatch(updateUrl(card, { dirty: false }));
 
         return card;
@@ -466,7 +474,7 @@ export const reloadCard = createThunkAction(RELOAD_CARD, () => {
 });
 
 // setCardAndRun
-export const SET_CARD_AND_RUN = "SET_CARD_AND_RUN";
+export const SET_CARD_AND_RUN = "metabase/qb/SET_CARD_AND_RUN";
 export const setCardAndRun = createThunkAction(SET_CARD_AND_RUN, (runCard, shouldUpdateUrl = true) => {
     return async (dispatch, getState) => {
         // clone
@@ -474,16 +482,16 @@ export const setCardAndRun = createThunkAction(SET_CARD_AND_RUN, (runCard, shoul
 
         dispatch(loadMetadataForCard(card));
 
-        dispatch(runQuery(card, shouldUpdateUrl));
+        dispatch(runQuery(card, { shouldUpdateUrl: shouldUpdateUrl }));
 
         return card;
     };
 });
 
 
-// setQuery
-export const SET_QUERY = "SET_QUERY";
-export const setQuery = createThunkAction(SET_QUERY, (dataset_query, run = false) => {
+// setDatasetQuery
+export const SET_DATASET_QUERY = "metabase/qb/SET_DATASET_QUERY";
+export const setDatasetQuery = createThunkAction(SET_DATASET_QUERY, (dataset_query, run = false) => {
     return (dispatch, getState) => {
         const { qb: { card, uiControls, databases } } = getState();
 
@@ -585,7 +593,7 @@ export const setQuery = createThunkAction(SET_QUERY, (dataset_query, run = false
 });
 
 // setQueryMode
-export const SET_QUERY_MODE = "SET_QUERY_MODE";
+export const SET_QUERY_MODE = "metabase/qb/SET_QUERY_MODE";
 export const setQueryMode = createThunkAction(SET_QUERY_MODE, (type) => {
     return (dispatch, getState) => {
         const { qb: { card, queryResult, tableMetadata, uiControls } } = getState();
@@ -649,7 +657,7 @@ export const setQueryMode = createThunkAction(SET_QUERY_MODE, (type) => {
 });
 
 // setQueryDatabase
-export const SET_QUERY_DATABASE = "SET_QUERY_DATABASE";
+export const SET_QUERY_DATABASE = "metabase/qb/SET_QUERY_DATABASE";
 export const setQueryDatabase = createThunkAction(SET_QUERY_DATABASE, (databaseId) => {
     return async (dispatch, getState) => {
         const { qb: { card, databases, uiControls } } = getState();
@@ -697,7 +705,7 @@ export const setQueryDatabase = createThunkAction(SET_QUERY_DATABASE, (databaseI
 });
 
 // setQuerySourceTable
-export const SET_QUERY_SOURCE_TABLE = "SET_QUERY_SOURCE_TABLE";
+export const SET_QUERY_SOURCE_TABLE = "metabase/qb/SET_QUERY_SOURCE_TABLE";
 export const setQuerySourceTable = createThunkAction(SET_QUERY_SOURCE_TABLE, (sourceTable) => {
     return async (dispatch, getState) => {
         const { qb: { card, uiControls } } = getState();
@@ -744,54 +752,109 @@ export const setQuerySourceTable = createThunkAction(SET_QUERY_SOURCE_TABLE, (so
     };
 });
 
-// setQuerySort
-export const SET_QUERY_SORT = "SET_QUERY_SORT";
-export const setQuerySort = createThunkAction(SET_QUERY_SORT, (column) => {
-    return (dispatch, getState) => {
-        const { qb: { card } } = getState();
-
-        // NOTE: we only allow this for structured type queries & we only allow sorting by a single column
-        if (card.dataset_query.type === "query") {
-            let field = null;
-            if (column.id == null) {
-                // ICK.  this is hacky for dealing with aggregations.  need something better
-                // DOUBLE ICK.  we also need to deal with custom fields now as well
-                if (_.contains(_.keys(Query.getExpressions(card.dataset_query.query)), column.display_name)) {
-                    field = ["expression", column.display_name];
-                } else {
-                    field = ["aggregation", 0];
-                }
-            } else {
-                field = column.id;
-            }
-
-            let dataset_query = Utils.copy(card.dataset_query),
-                sortClause = [field, "ascending"];
-
-            if (card.dataset_query.query.order_by &&
-                card.dataset_query.query.order_by.length > 0 &&
-                card.dataset_query.query.order_by[0].length > 0 &&
-                card.dataset_query.query.order_by[0][1] === "ascending" &&
-                Query.isSameField(card.dataset_query.query.order_by[0][0], field)) {
-                // someone triggered another sort on the same column, so flip the sort direction
-                sortClause = [field, "descending"];
+function createQueryAction(action, updaterFunction, event) {
+    return createThunkAction(action, (...args) =>
+        (dispatch, getState) => {
+            const { qb: { card } } = getState();
+            if (card.dataset_query.type === "query") {
+                const datasetQuery = Utils.copy(card.dataset_query);
+                updaterFunction(datasetQuery.query, ...args);
+                dispatch(setDatasetQuery(datasetQuery));
+                MetabaseAnalytics.trackEvent(...(typeof event === "function" ? event(...args) : event));
             }
-
-            // set clause
-            dataset_query.query.order_by = [sortClause];
-
-            // update and run the query
-            dispatch(setQuery(dataset_query, true));
+            return null;
         }
+    );
+}
 
-        return null;
-    };
-});
-
+export const addQueryBreakout = createQueryAction(
+    "metabase/qb/ADD_QUERY_BREAKOUT",
+    Query.addBreakout,
+    ["QueryBuilder", "Add GroupBy"]
+);
+export const updateQueryBreakout = createQueryAction(
+    "metabase/qb/UPDATE_QUERY_BREAKOUT",
+    Query.updateBreakout,
+    ["QueryBuilder", "Modify GroupBy"]
+);
+export const removeQueryBreakout = createQueryAction(
+    "metabase/qb/REMOVE_QUERY_BREAKOUT",
+    Query.removeBreakout,
+    ["QueryBuilder", "Remove GroupBy"]
+);
+export const addQueryFilter = createQueryAction(
+    "metabase/qb/ADD_QUERY_FILTER",
+    Query.addFilter,
+    ["QueryBuilder", "Add Filter"]
+);
+export const updateQueryFilter = createQueryAction(
+    "metabase/qb/UPDATE_QUERY_FILTER",
+    Query.updateFilter,
+    ["QueryBuilder", "Modify Filter"]
+);
+export const removeQueryFilter = createQueryAction(
+    "metabase/qb/REMOVE_QUERY_FILTER",
+    Query.removeFilter,
+    ["QueryBuilder", "Remove Filter"]
+);
+export const addQueryAggregation = createQueryAction(
+    "metabase/qb/ADD_QUERY_AGGREGATION",
+    Query.addAggregation,
+    ["QueryBuilder", "Add Aggregation"]
+);
+export const updateQueryAggregation = createQueryAction(
+    "metabase/qb/UPDATE_QUERY_AGGREGATION",
+    Query.updateAggregation,
+    ["QueryBuilder", "Set Aggregation"]
+);
+export const removeQueryAggregation = createQueryAction(
+    "metabase/qb/REMOVE_QUERY_AGGREGATION",
+    Query.removeAggregation,
+    ["QueryBuilder", "Remove Aggregation"]
+);
+export const addQueryOrderBy = createQueryAction(
+    "metabase/qb/ADD_QUERY_ORDER_BY",
+    Query.addOrderBy,
+    ["QueryBuilder", "Add OrderBy"]
+);
+export const updateQueryOrderBy = createQueryAction(
+    "metabase/qb/UPDATE_QUERY_ORDER_BY",
+    Query.updateOrderBy,
+    ["QueryBuilder", "Set OrderBy"]
+);
+export const removeQueryOrderBy = createQueryAction(
+    "metabase/qb/REMOVE_QUERY_ORDER_BY",
+    Query.removeOrderBy,
+    ["QueryBuilder", "Remove OrderBy"]
+);
+export const updateQueryLimit = createQueryAction(
+    "metabase/qb/UPDATE_QUERY_LIMIT",
+    Query.updateLimit,
+    ["QueryBuilder", "Update Limit"]
+);
+export const addQueryExpression = createQueryAction(
+    "metabase/qb/ADD_QUERY_EXPRESSION",
+    Query.addExpression,
+    ["QueryBuilder", "Add Expression"]
+);
+export const updateQueryExpression = createQueryAction(
+    "metabase/qb/UPDATE_QUERY_EXPRESSION",
+    Query.updateExpression,
+    ["QueryBuilder", "Set Expression"]
+);
+export const removeQueryExpression = createQueryAction(
+    "metabase/qb/REMOVE_QUERY_EXPRESSION",
+    Query.removeExpression,
+    ["QueryBuilder", "Remove Expression"]
+);
 
 // runQuery
-export const RUN_QUERY = "RUN_QUERY";
-export const runQuery = createThunkAction(RUN_QUERY, (card, shouldUpdateUrl = true, parameterValues, dirty) => {
+export const RUN_QUERY = "metabase/qb/RUN_QUERY";
+export const runQuery = createThunkAction(RUN_QUERY, (card, {
+    shouldUpdateUrl = true,
+    ignoreCache = false, // currently only implemented for saved cards
+    parameterValues
+} = {}) => {
     return async (dispatch, getState) => {
         const state = getState();
         const parameters = getParameters(state);
@@ -802,11 +865,6 @@ export const runQuery = createThunkAction(RUN_QUERY, (card, shouldUpdateUrl = tr
 
         const cardIsDirty = isCardDirty(card, state.qb.originalCard);
 
-        card = {
-            ...card,
-            dataset_query: applyParameters(card, parameters, parameterValues)
-        };
-
         if (shouldUpdateUrl) {
             dispatch(updateUrl(card, { dirty: cardIsDirty }));
         }
@@ -823,11 +881,17 @@ export const runQuery = createThunkAction(RUN_QUERY, (card, shouldUpdateUrl = tr
             dispatch(queryErrored(startTime, error));
         }
 
+        const datasetQuery = applyParameters(card, parameters, parameterValues);
+
         // use the CardApi.query if the query is saved and not dirty so users with view but not create permissions can see it.
-        if (card && card.id && !isDirty(state) && dirty !== true) {
-            CardApi.query({ cardId: card.id, parameters: card.dataset_query.parameters }, { cancelled: cancelQueryDeferred.promise }).then(onQuerySuccess, onQueryError);
+        if (card.id != null && !cardIsDirty) {
+            CardApi.query({
+                cardId: card.id,
+                parameters: datasetQuery.parameters,
+                ignore_cache: ignoreCache
+            }, { cancelled: cancelQueryDeferred.promise }).then(onQuerySuccess, onQueryError);
         } else {
-            MetabaseApi.dataset(card.dataset_query, { cancelled: cancelQueryDeferred.promise }).then(onQuerySuccess, onQueryError);
+            MetabaseApi.dataset(datasetQuery, { cancelled: cancelQueryDeferred.promise }).then(onQuerySuccess, onQueryError);
         }
 
         MetabaseAnalytics.trackEvent("QueryBuilder", "Run Query", card.dataset_query.type);
@@ -839,7 +903,7 @@ export const runQuery = createThunkAction(RUN_QUERY, (card, shouldUpdateUrl = tr
     };
 });
 
-export const QUERY_COMPLETED = "QUERY_COMPLETED";
+export const QUERY_COMPLETED = "metabase/qb/QUERY_COMPLETED";
 export const queryCompleted = createThunkAction(QUERY_COMPLETED, (card, queryResult) => {
     return async (dispatch, getState) => {
         let cardDisplay = card.display;
@@ -850,13 +914,13 @@ export const queryCompleted = createThunkAction(QUERY_COMPLETED, (card, queryRes
         if (!isScalarVisualization &&
                 queryResult.data.rows &&
                 queryResult.data.rows.length === 1 &&
-                queryResult.data.columns.length === 1) {
+                queryResult.data.cols.length === 1) {
             // if we have a 1x1 data result then this should always be viewed as a scalar
             cardDisplay = "scalar";
 
         } else if (isScalarVisualization &&
                     queryResult.data.rows &&
-                    (queryResult.data.rows.length > 1 || queryResult.data.columns.length > 1)) {
+                    (queryResult.data.rows.length > 1 || queryResult.data.cols.length > 1)) {
             // any time we were a scalar and now have more than 1x1 data switch to table view
             cardDisplay = "table";
 
@@ -866,13 +930,14 @@ export const queryCompleted = createThunkAction(QUERY_COMPLETED, (card, queryRes
         }
 
         return {
+            card,
             cardDisplay,
             queryResult
         }
     };
 });
 
-export const QUERY_ERRORED = "QUERY_ERRORED";
+export const QUERY_ERRORED = "metabase/qb/QUERY_ERRORED";
 export const queryErrored = createThunkAction(QUERY_ERRORED, (startTime, error) => {
     return async (dispatch, getState) => {
         if (error && error.status === 0) {
@@ -885,7 +950,7 @@ export const queryErrored = createThunkAction(QUERY_ERRORED, (startTime, error)
 })
 
 // cancelQuery
-export const CANCEL_QUERY = "CANCEL_QUERY";
+export const CANCEL_QUERY = "metabase/qb/CANCEL_QUERY";
 export const cancelQuery = createThunkAction(CANCEL_QUERY, () => {
     return async (dispatch, getState) => {
         const { qb: { uiControls, queryExecutionPromise } } = getState();
@@ -897,7 +962,7 @@ export const cancelQuery = createThunkAction(CANCEL_QUERY, () => {
 });
 
 // cellClicked
-export const CELL_CLICKED = "CELL_CLICKED";
+export const CELL_CLICKED = "metabase/qb/CELL_CLICKED";
 export const cellClicked = createThunkAction(CELL_CLICKED, (rowIndex, columnIndex, filter) => {
     return async (dispatch, getState) => {
         const { qb: { card, queryResult } } = getState();
@@ -956,14 +1021,14 @@ export const cellClicked = createThunkAction(CELL_CLICKED, (rowIndex, columnInde
             }
 
             // update and run the query
-            dispatch(setQuery(dataset_query, true));
+            dispatch(setDatasetQuery(dataset_query, true));
 
             MetabaseAnalytics.trackEvent("QueryBuilder", "Table Cell Click", "Quick Filter");
         }
     };
 });
 
-export const FOLLOW_FOREIGN_KEY = "FOLLOW_FOREIGN_KEY";
+export const FOLLOW_FOREIGN_KEY = "metabase/qb/FOLLOW_FOREIGN_KEY";
 export const followForeignKey = createThunkAction(FOLLOW_FOREIGN_KEY, (fk) => {
     return async (dispatch, getState) => {
         const { qb: { card, queryResult } } = getState();
@@ -991,7 +1056,7 @@ export const followForeignKey = createThunkAction(FOLLOW_FOREIGN_KEY, (fk) => {
 });
 
 
-export const LOAD_OBJECT_DETAIL_FK_REFERENCES = "LOAD_OBJECT_DETAIL_FK_REFERENCES";
+export const LOAD_OBJECT_DETAIL_FK_REFERENCES = "metabase/qb/LOAD_OBJECT_DETAIL_FK_REFERENCES";
 export const loadObjectDetailFKReferences = createThunkAction(LOAD_OBJECT_DETAIL_FK_REFERENCES, () => {
     return async (dispatch, getState) => {
         const { qb: { card, queryResult, tableForeignKeys } } = getState();
@@ -1051,10 +1116,6 @@ export const toggleDataReferenceFn = toggleDataReference;
 export const onBeginEditing = beginEditing;
 export const onCancelEditing = cancelEditing;
 export const setQueryModeFn = setQueryMode;
-export const setSortFn = setQuerySort;
-export const setQueryFn = setQuery;
-export const runQueryFn = runQuery;
-export const cancelQueryFn = cancelQuery;
 export const setDatabaseFn = setQueryDatabase;
 export const setSourceTableFn = setQuerySourceTable;
 export const setDisplayFn = setCardVisualization;
diff --git a/frontend/src/metabase/query_builder/components/ActionsWidget.jsx b/frontend/src/metabase/query_builder/components/ActionsWidget.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..fccb165d952d715624ed713e5424272bf87efe36
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/ActionsWidget.jsx
@@ -0,0 +1,181 @@
+/* @flow */
+
+import React, { Component, PropTypes } from "react";
+
+import Icon from "metabase/components/Icon";
+import OnClickOutsideWrapper from "metabase/components/OnClickOutsideWrapper";
+
+import { getModeActions } from "metabase/qb/lib/modes";
+
+import cx from "classnames";
+import _ from "underscore";
+
+import type { Card } from "metabase/meta/types/Card";
+import type { QueryMode, ClickAction } from "metabase/meta/types/Visualization";
+import type { TableMetadata } from "metabase/meta/types/Metadata";
+
+type Props = {
+    className?: string,
+    mode: QueryMode,
+    card: Card,
+    tableMetadata: TableMetadata,
+    setCardAndRun: (card: Card) => void
+};
+
+const CIRCLE_SIZE = 48;
+const NEEDLE_SIZE = 20;
+const POPOVER_WIDTH = 350;
+
+export default class ActionsWidget extends Component<*, Props, *> {
+    state = {
+        isVisible: false,
+        isOpen: false,
+        selectedActionIndex: null
+    };
+
+    componentWillMount() {
+        window.addEventListener("mousemove", this.handleMouseMoved, false);
+    }
+
+    componentWillUnmount() {
+        window.removeEventListener("mousemove", this.handleMouseMoved, false);
+    }
+
+    handleMouseMoved = () => {
+        if (!this.state.isVisible) {
+            this.setState({ isVisible: true });
+        }
+        this.handleMouseStoppedMoving();
+    };
+
+    handleMouseStoppedMoving = _.debounce(
+        () => {
+            this.setState({ isVisible: false });
+        },
+        1000
+    );
+
+    close = () => {
+        this.setState({ isOpen: false, selectedActionIndex: null });
+    };
+
+    toggle = () => {
+        this.setState({
+            isOpen: !this.state.isOpen,
+            selectedActionIndex: null
+        });
+    };
+
+    handleActionClick = (index: number) => {
+        const { mode, card, tableMetadata } = this.props;
+        const action = getModeActions(mode, card, tableMetadata)[index];
+        if (action && action.popover) {
+            this.setState({ selectedActionIndex: index });
+        } else if (action && action.card) {
+            const card = action.card();
+            if (card) {
+                this.props.setCardAndRun(card);
+            }
+            this.close();
+        }
+    };
+    render() {
+        const { className, mode, card, tableMetadata } = this.props;
+        const { isOpen, isVisible, selectedActionIndex } = this.state;
+
+        const actions: ClickAction[] = getModeActions(mode, card, tableMetadata);
+        if (actions.length === 0) {
+            return null;
+        }
+
+        const selectedAction: ?ClickAction = selectedActionIndex == null ? null :
+            actions[selectedActionIndex];
+        let PopoverComponent = selectedAction && selectedAction.popover;
+
+        return (
+            <div className={cx(className, "relative")}>
+                <div
+                    className="circular bg-brand flex layout-centered m4 cursor-pointer"
+                    style={{
+                        width: CIRCLE_SIZE,
+                        height: CIRCLE_SIZE,
+                        transition: "opacity 300ms ease-in-out",
+                        opacity: isOpen || isVisible ? 1 : 0,
+                        boxShadow: "2px 2px 4px rgba(0, 0, 0, 0.2)"
+                    }}
+                    onClick={this.toggle}
+                >
+                    <Icon
+                        name="compass_needle"
+                        className="text-white"
+                        style={{
+                            transition: "transform 500ms ease-in-out",
+                            transform: isOpen
+                                ? "rotate(0deg)"
+                                : "rotate(720deg)"
+                        }}
+                        size={NEEDLE_SIZE}
+                    />
+                </div>
+                {isOpen &&
+                    <OnClickOutsideWrapper handleDismissal={this.close}>
+                        <div
+                            className="absolute bg-white rounded bordered shadowed py1"
+                            style={{
+                                width: POPOVER_WIDTH,
+                                bottom: "50%",
+                                right: "50%",
+                                zIndex: -1
+                            }}
+                        >
+                            {PopoverComponent
+                                ? <div>
+                                      <div
+                                          className="flex align-center text-grey-4 p1 px2"
+                                      >
+                                          <Icon
+                                              name="chevronleft"
+                                              className="cursor-pointer"
+                                              onClick={() => this.setState({
+                                                  selectedActionIndex: null
+                                              })}
+                                          />
+                                          <div
+                                              className="text-centered flex-full"
+                                          >
+                                              {selectedAction && selectedAction.title}
+                                          </div>
+                                      </div>
+                                      <PopoverComponent
+                                          onChangeCardAndRun={(card) => {
+                                              if (card) {
+                                                  this.props.setCardAndRun(card);
+                                              }
+                                          }}
+                                          onClose={this.close}
+                                      />
+                                  </div>
+                                : actions.map((action, index) => (
+                                      <div
+                                          key={index}
+                                          className="p2 flex align-center text-grey-4 brand-hover cursor-pointer"
+                                          onClick={() =>
+                                              this.handleActionClick(index)}
+                                      >
+                                          {action.icon &&
+                                              <Icon
+                                                  name={action.icon}
+                                                  className="mr1 flex-no-shrink"
+                                                  size={16}
+                                              />}
+                                          <div>
+                                              {action.title}
+                                          </div>
+                                      </div>
+                                  ))}
+                        </div>
+                    </OnClickOutsideWrapper>}
+            </div>
+        );
+    }
+}
diff --git a/frontend/src/metabase/query_builder/components/AggregationPopover.jsx b/frontend/src/metabase/query_builder/components/AggregationPopover.jsx
index df3c35f94413985d35fefece5e6d9bf09247c0e7..d02705b9ebcca688e4dcee8dd40e0b7561e9c894 100644
--- a/frontend/src/metabase/query_builder/components/AggregationPopover.jsx
+++ b/frontend/src/metabase/query_builder/components/AggregationPopover.jsx
@@ -33,11 +33,12 @@ export default class AggregationPopover extends Component {
     static propTypes = {
         isNew: PropTypes.bool,
         aggregation: PropTypes.array,
-        availableAggregations: PropTypes.array.isRequired,
         onCommitAggregation: PropTypes.func.isRequired,
         onClose: PropTypes.func.isRequired,
         tableMetadata: PropTypes.object.isRequired,
-        customFields: PropTypes.object
+        datasetQuery: PropTypes.object,
+        customFields: PropTypes.object,
+        availableAggregations: PropTypes.array,
     };
 
 
@@ -75,9 +76,20 @@ export default class AggregationPopover extends Component {
         });
     }
 
+    getAvailableAggregations() {
+        const { availableAggregations, tableMetadata } = this.props;
+        return availableAggregations || (tableMetadata && tableMetadata.aggregation_options)
+    }
+
+    getCustomFields() {
+        const { customFields, datasetQuery } = this.props;
+        return customFields || (datasetQuery && Query.getExpressions(datasetQuery.query));
+    }
+
     getAggregationFieldOptions(aggOperator) {
+        const availableAggregations = this.getAvailableAggregations();
         // NOTE: we don't use getAggregator() here because availableAggregations has the table.fields populated on the aggregation options
-        const aggOptions = this.props.availableAggregations.filter((o) => o.short === aggOperator);
+        const aggOptions = availableAggregations.filter((o) => o.short === aggOperator);
         if (aggOptions && aggOptions.length > 0) {
             return Query.getFieldOptions(this.props.tableMetadata.fields, true, aggOptions[0].validFieldsFilters[0])
         }
@@ -114,7 +126,11 @@ export default class AggregationPopover extends Component {
     }
 
     render() {
-        const { availableAggregations, tableMetadata } = this.props;
+        const { tableMetadata } = this.props;
+
+        const customFields = this.getCustomFields();
+        const availableAggregations = this.getAvailableAggregations();
+
         const { choosingField, editingAggregation } = this.state;
         const aggregation = NamedClause.getContent(this.state.aggregation);
 
@@ -182,7 +198,7 @@ export default class AggregationPopover extends Component {
                             startRule="aggregation"
                             expression={aggregation}
                             tableMetadata={tableMetadata}
-                            customFields={this.props.customFields}
+                            customFields={customFields}
                             onChange={(parsedExpression) => this.setState({
                                 aggregation: NamedClause.setContent(this.state.aggregation, parsedExpression),
                                 error: null
@@ -230,7 +246,7 @@ export default class AggregationPopover extends Component {
                         tableMetadata={tableMetadata}
                         field={fieldId}
                         fieldOptions={this.getAggregationFieldOptions(agg)}
-                        customFieldOptions={this.props.customFields}
+                        customFieldOptions={customFields}
                         onFieldChange={this.onPickField}
                         enableTimeGrouping={false}
                     />
diff --git a/frontend/src/metabase/query_builder/components/DataSelector.jsx b/frontend/src/metabase/query_builder/components/DataSelector.jsx
index 55f12b87d360e6489575e18f85f9c97422267001..24504e412ba6229f7f328998edaede93e7f73663 100644
--- a/frontend/src/metabase/query_builder/components/DataSelector.jsx
+++ b/frontend/src/metabase/query_builder/components/DataSelector.jsx
@@ -25,7 +25,7 @@ export default class DataSelector extends Component {
     }
 
     static propTypes = {
-        query: PropTypes.object.isRequired,
+        datasetQuery: PropTypes.object.isRequired,
         databases: PropTypes.array.isRequired,
         tables: PropTypes.array,
         segments: PropTypes.array,
@@ -50,7 +50,7 @@ export default class DataSelector extends Component {
     }
 
     componentWillReceiveProps(newProps) {
-        let tableId = newProps.query.query && newProps.query.query.source_table;
+        let tableId = newProps.datasetQuery.query && newProps.datasetQuery.query.source_table;
         let selectedSchema;
         // augment databases with schemas
         let databases = newProps.databases && newProps.databases.map(database => {
@@ -137,15 +137,15 @@ export default class DataSelector extends Component {
     }
 
     getSegmentId() {
-        return this.props.query.segment;
+        return this.props.datasetQuery.segment;
     }
 
     getDatabaseId() {
-        return this.props.query.database;
+        return this.props.datasetQuery.database;
     }
 
     getTableId() {
-        return this.props.query.query && this.props.query.query.source_table;
+        return this.props.datasetQuery.query && this.props.datasetQuery.query.source_table;
     }
 
     renderDatabasePicker() {
diff --git a/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx b/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx
index 43bf3544f570ecfeb43f0c9e990da516c73dfdf0..b5c13ee78ad4490114ded991d31ec7818e5ee9d3 100644
--- a/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx
+++ b/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx
@@ -14,26 +14,16 @@ import Query from "metabase/lib/query";
 
 
 export default class ExtendedOptions extends Component {
-
-    constructor(props, context) {
-        super(props, context);
-
-        this.state = {
-            isOpen: false,
-            editExpression: null
-        };
-
-        _.bindAll(
-            this,
-            "setLimit", "addOrderBy", "updateOrderBy", "removeOrderBy"
-        );
-    }
+    state = {
+        isOpen: false,
+        editExpression: null
+    };
 
     static propTypes = {
         features: PropTypes.object.isRequired,
-        query: PropTypes.object.isRequired,
+        datasetQuery: PropTypes.object.isRequired,
         tableMetadata: PropTypes.object,
-        setQuery: PropTypes.func.isRequired
+        setDatasetQuery: PropTypes.func.isRequired
     };
 
     static defaultProps = {
@@ -41,41 +31,8 @@ export default class ExtendedOptions extends Component {
     };
 
 
-    setLimit(limit) {
-        if (limit) {
-            Query.updateLimit(this.props.query.query, limit);
-            MetabaseAnalytics.trackEvent('QueryBuilder', 'Set Limit');
-        } else {
-            Query.clearLimit(this.props.query.query);
-            MetabaseAnalytics.trackEvent('QueryBuilder', 'Remove Limit');
-        }
-        this.props.setQuery(this.props.query);
-        this.setState({isOpen: false});
-    }
-
-    addOrderBy() {
-        Query.addOrderBy(this.props.query.query, [null, "ascending"]);
-        this.props.setQuery(this.props.query);
-
-        MetabaseAnalytics.trackEvent('QueryBuilder', 'Set Sort', 'manual');
-    }
-
-    updateOrderBy(index, sort) {
-        Query.updateOrderBy(this.props.query.query, index, sort);
-        this.props.setQuery(this.props.query);
-
-        MetabaseAnalytics.trackEvent('QueryBuilder', 'Set Sort', 'manual');
-    }
-
-    removeOrderBy(index) {
-        Query.removeOrderBy(this.props.query.query, index);
-        this.props.setQuery(this.props.query);
-
-        MetabaseAnalytics.trackEvent('QueryBuilder', 'Remove Sort');
-    }
-
     setExpression(name, expression, previousName) {
-        let query = this.props.query.query;
+        const { datasetQuery: { query } } = this.props;
 
         if (!_.isEmpty(previousName)) {
             // remove old expression using original name.  this accounts for case where expression is renamed.
@@ -84,23 +41,23 @@ export default class ExtendedOptions extends Component {
 
         // now add the new expression to the query
         Query.setExpression(query, name, expression);
-        this.props.setQuery(this.props.query);
+        this.props.setDatasetQuery(this.props.datasetQuery);
         this.setState({editExpression: null});
 
         MetabaseAnalytics.trackEvent('QueryBuilder', 'Set Expression', !_.isEmpty(previousName));
     }
 
     removeExpression(name) {
-        let scrubbedQuery = Query.removeExpression(this.props.query.query, name);
-        this.props.query.query = scrubbedQuery;
-        this.props.setQuery(this.props.query);
+        let scrubbedQuery = Query.removeExpression(this.props.datasetQuery.query, name);
+        this.props.datasetQuery.query = scrubbedQuery;
+        this.props.setDatasetQuery(this.props.datasetQuery);
         this.setState({editExpression: null});
 
         MetabaseAnalytics.trackEvent('QueryBuilder', 'Remove Expression');
     }
 
     renderSort() {
-        const { query: { query }, tableMetadata } = this.props;
+        const { datasetQuery: { query }, tableMetadata } = this.props;
 
         if (!this.props.features.limit) {
             return;
@@ -118,7 +75,7 @@ export default class ExtendedOptions extends Component {
                 if (Query.isExpressionField(sort[0])) {
                     usedExpressions[sort[0][1]] = true;
                 } else {
-                    usedFields[sort[0]] = true;
+                    usedFields[Query.getFieldTargetId(sort[0])] = true;
                 }
             }
 
@@ -136,8 +93,8 @@ export default class ExtendedOptions extends Component {
                         )
                     }
                     customFieldOptions={expressions}
-                    removeOrderBy={this.removeOrderBy.bind(null, index)}
-                    updateOrderBy={this.updateOrderBy.bind(null, index)}
+                    removeOrderBy={() => this.props.removeQueryOrderBy(index)}
+                    updateOrderBy={(orderBy) => this.props.updateQueryOrderBy(index, orderBy)}
                 />
             );
 
@@ -151,7 +108,7 @@ export default class ExtendedOptions extends Component {
             const remainingExpressions = Object.keys(_.omit(expressions, usedExpressions));
             if ((remainingFieldOptions.count > 0 || remainingExpressions.length > 1) &&
                 (sorts.length === 0 || sorts[sorts.length - 1][0] != null)) {
-                addSortButton = (<AddClauseButton text="Pick a field to sort by" onClick={this.addOrderBy} />);
+                addSortButton = (<AddClauseButton text="Pick a field to sort by" onClick={() => this.props.addQueryOrderBy([null, "ascending"])} />);
             }
         }
 
@@ -170,7 +127,7 @@ export default class ExtendedOptions extends Component {
         // if we aren't editing any expression then there is nothing to do
         if (!this.state.editExpression || !this.props.tableMetadata) return null;
 
-        const query = this.props.query.query,
+        const query = this.props.datasetQuery.query,
               expressions = Query.getExpressions(query),
               expression = expressions && expressions[this.state.editExpression],
               name = _.isString(this.state.editExpression) ? this.state.editExpression : "";
@@ -192,7 +149,7 @@ export default class ExtendedOptions extends Component {
     renderPopover() {
         if (!this.state.isOpen) return null;
 
-        const { features, query, tableMetadata } = this.props;
+        const { features, datasetQuery, tableMetadata } = this.props;
 
         return (
             <Popover onClose={() => this.setState({isOpen: false})}>
@@ -201,7 +158,7 @@ export default class ExtendedOptions extends Component {
 
                     {_.contains(tableMetadata.db.features, "expressions") ?
                         <Expressions
-                            expressions={query.query.expressions}
+                            expressions={datasetQuery.query.expressions}
                             tableMetadata={tableMetadata}
                             onAddExpression={() => this.setState({isOpen: false, editExpression: true})}
                             onEditExpression={(name) => {
@@ -214,7 +171,10 @@ export default class ExtendedOptions extends Component {
                     { features.limit &&
                         <div>
                             <div className="mb1 h6 text-uppercase text-grey-3 text-bold">Row limit</div>
-                            <LimitWidget limit={query.query.limit} onChange={this.setLimit} />
+                            <LimitWidget limit={datasetQuery.query.limit} onChange={(limit) => {
+                                this.props.updateQueryLimit(limit);
+                                this.setState({ isOpen: false })
+                            }} />
                         </div>
                     }
                 </div>
diff --git a/frontend/src/metabase/query_builder/components/FieldList.jsx b/frontend/src/metabase/query_builder/components/FieldList.jsx
index b4ee66a8c51e9fd049554cf681afbac498377c4f..143ad547f31595ec1e518f5f7789ddfdc3c12728 100644
--- a/frontend/src/metabase/query_builder/components/FieldList.jsx
+++ b/frontend/src/metabase/query_builder/components/FieldList.jsx
@@ -8,7 +8,7 @@ import TimeGroupingPopover from "./TimeGroupingPopover.jsx";
 import QueryDefinitionTooltip from "./QueryDefinitionTooltip.jsx";
 
 import { isDate, getIconForField } from 'metabase/lib/schema_metadata';
-import { parseFieldBucketing, parseFieldTarget } from "metabase/lib/query_time";
+import { parseFieldBucketing, parseFieldTargetId } from "metabase/lib/query_time";
 import { stripId, singularize } from "metabase/lib/formatting";
 import Query from "metabase/lib/query";
 
@@ -63,7 +63,7 @@ export default class FieldList extends Component {
             name: singularize(tableName),
             items: specialOptions.concat(fieldOptions.fields.map(field => ({
                 name: Query.getFieldPathName(field.id, tableMetadata),
-                value: ["field-id", field.id],
+                value: typeof field.id === "number" ? ["field-id", field.id] : field.id,
                 field: field
             })))
         };
@@ -81,8 +81,13 @@ export default class FieldList extends Component {
             })
         }));
 
-        let sections = [mainSection].concat(fkSections);
-        let fieldTarget = parseFieldTarget(field);
+        let sections = []
+        if (mainSection.items.length > 0) {
+            sections.push(mainSection);
+        }
+        sections.push(...fkSections);
+
+        let fieldTarget = parseFieldTargetId(field);
 
         this.setState({ sections, fieldTarget });
     }
@@ -120,8 +125,7 @@ export default class FieldList extends Component {
                         }}
                     >
                         <TimeGroupingPopover
-                            field={field}
-                            value={item.value}
+                            field={field || ["datetime-field", item.value, "as", null]}
                             onFieldChange={this.props.onFieldChange}
                             groupingOptions={item.field.grouping_options}
                         />
@@ -204,6 +208,7 @@ export default class FieldList extends Component {
                 renderItemExtra={this.renderItemExtra}
                 renderItemIcon={this.renderItemIcon}
                 getItemClasses={this.getItemClasses}
+                alwaysExpanded={this.props.alwaysExpanded}
             />
         )
     }
diff --git a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
index 2d8a36641f3b5ab4b75678405a817bcc3b839e09..d6d99f521255d3c2346f567e42fcf0597c5dd8d8 100644
--- a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
+++ b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
@@ -11,7 +11,6 @@ import Icon from "metabase/components/Icon.jsx";
 import IconBorder from 'metabase/components/IconBorder.jsx';
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
 
-import MetabaseAnalytics from 'metabase/lib/analytics';
 import Query from "metabase/lib/query";
 
 import cx from "classnames";
@@ -25,19 +24,14 @@ export default class GuiQueryEditor extends Component {
         this.state = {
             expanded: true
         };
-
-        _.bindAll(
-            this,
-            "setBreakout",
-        );
     }
 
     static propTypes = {
         databases: PropTypes.array,
-        query: PropTypes.object.isRequired,
+        datasetQuery: PropTypes.object.isRequired,
         tableMetadata: PropTypes.object, // can't be required, sometimes null
         isShowingDataReference: PropTypes.bool.isRequired,
-        setQueryFn: PropTypes.func.isRequired,
+        setDatasetQuery: PropTypes.func.isRequired,
         setDatabaseFn: PropTypes.func,
         setSourceTableFn: PropTypes.func,
         features: PropTypes.object
@@ -54,64 +48,6 @@ export default class GuiQueryEditor extends Component {
         }
     };
 
-    setQuery(datasetQuery) {
-        this.props.setQueryFn(datasetQuery);
-    }
-
-    setBreakout = (index, field) => {
-        if (field == null) {
-            Query.removeBreakout(this.props.query.query, index);
-            this.setQuery(this.props.query);
-            MetabaseAnalytics.trackEvent('QueryBuilder', 'Remove GroupBy');
-        } else {
-            if (index > Query.getBreakouts(this.props.query.query) - 1) {
-                Query.addBreakout(this.props.query.query, field);
-                this.setQuery(this.props.query);
-                MetabaseAnalytics.trackEvent('QueryBuilder', 'Add GroupBy');
-            } else {
-                Query.updateBreakout(this.props.query.query, index, field);
-                this.setQuery(this.props.query);
-                MetabaseAnalytics.trackEvent('QueryBuilder', 'Modify GroupBy');
-            }
-        }
-    }
-
-    updateAggregation = (index, aggregationClause) => {
-        Query.updateAggregation(this.props.query.query, index, aggregationClause);
-        this.setQuery(this.props.query);
-
-        MetabaseAnalytics.trackEvent('QueryBuilder', 'Set Aggregation', aggregationClause[0]);
-    }
-
-    removeAggregation = (index, aggregationClause) => {
-        Query.removeAggregation(this.props.query.query, index);
-        this.setQuery(this.props.query);
-
-        MetabaseAnalytics.trackEvent('QueryBuilder', 'Remove Aggregation', aggregationClause[0]);
-    }
-
-    addFilter = (filter) => {
-        const query = this.props.query;
-        Query.addFilter(query.query, filter);
-        this.setQuery(query);
-
-        MetabaseAnalytics.trackEvent('QueryBuilder', 'Add Filter');
-    }
-
-    updateFilter = (index, filter) => {
-        Query.updateFilter(this.props.query.query, index, filter);
-        this.setQuery(this.props.query);
-
-        MetabaseAnalytics.trackEvent('QueryBuilder', 'Modify Filter');
-    }
-
-    removeFilter = (index) => {
-        Query.removeFilter(this.props.query.query, index);
-        this.setQuery(this.props.query);
-
-        MetabaseAnalytics.trackEvent('QueryBuilder', 'Remove Filter');
-    }
-
     renderAdd(text, onClick, targetRefName) {
         let className = "AddButton text-grey-2 text-bold flex align-center text-grey-4-hover cursor-pointer no-decoration transition-color";
         if (onClick) {
@@ -149,19 +85,19 @@ export default class GuiQueryEditor extends Component {
         if (this.props.tableMetadata) {
             enabled = true;
 
-            let filters = Query.getFilters(this.props.query.query);
+            let filters = Query.getFilters(this.props.datasetQuery.query);
             if (filters && filters.length > 0) {
                 filterList = (
                     <FilterList
                         filters={filters}
                         tableMetadata={this.props.tableMetadata}
-                        removeFilter={this.removeFilter}
-                        updateFilter={this.updateFilter}
+                        removeFilter={this.props.removeQueryFilter}
+                        updateFilter={this.props.updateQueryFilter}
                     />
                 );
             }
 
-            if (Query.canAddFilter(this.props.query.query)) {
+            if (Query.canAddFilter(this.props.datasetQuery.query)) {
                 addFilterButton = this.renderAdd((filterList ? null : "Add filters to narrow your answer"), null, "addFilterTarget");
             }
         } else {
@@ -186,8 +122,8 @@ export default class GuiQueryEditor extends Component {
                         <FilterPopover
                             isNew={true}
                             tableMetadata={this.props.tableMetadata || {}}
-                            customFields={Query.getExpressions(this.props.query.query)}
-                            onCommitFilter={this.addFilter}
+                            customFields={Query.getExpressions(this.props.datasetQuery.query)}
+                            onCommitFilter={this.props.addQueryFilter}
                             onClose={() => this.refs.filterPopover.close()}
                         />
                     </PopoverWithTrigger>
@@ -197,7 +133,7 @@ export default class GuiQueryEditor extends Component {
     }
 
     renderAggregation() {
-        const { query: { query }, tableMetadata } = this.props;
+        const { datasetQuery: { query }, tableMetadata } = this.props;
 
         if (!this.props.features.aggregation) {
             return;
@@ -226,9 +162,9 @@ export default class GuiQueryEditor extends Component {
                         key={"agg"+index}
                         aggregation={aggregation}
                         tableMetadata={tableMetadata}
-                        customFields={Query.getExpressions(this.props.query.query)}
-                        updateAggregation={(aggregation) => this.updateAggregation(index, aggregation)}
-                        removeAggregation={canRemoveAggregation ? this.removeAggregation.bind(null, index) : null}
+                        customFields={Query.getExpressions(this.props.datasetQuery.query)}
+                        updateAggregation={(aggregation) => this.props.updateQueryAggregation(index, aggregation)}
+                        removeAggregation={canRemoveAggregation ? this.props.removeQueryAggregation.bind(null, index) : null}
                         addButton={this.renderAdd(null)}
                     />
                 );
@@ -250,7 +186,7 @@ export default class GuiQueryEditor extends Component {
     }
 
     renderBreakouts() {
-        const { query: { query }, tableMetadata, features } = this.props;
+        const { datasetQuery: { query }, tableMetadata, features } = this.props;
 
         if (!features.breakout) {
             return;
@@ -264,7 +200,7 @@ export default class GuiQueryEditor extends Component {
 
             const usedFields = {};
             for (const breakout of breakouts) {
-                usedFields[breakout] = true;
+                usedFields[Query.getFieldTargetId(breakout)] = true;
             }
 
             const remainingFieldOptions = Query.getFieldOptions(tableMetadata.fields, true, tableMetadata.breakout_options.validFieldsFilter, usedFields);
@@ -287,7 +223,7 @@ export default class GuiQueryEditor extends Component {
                         customFieldOptions={Query.getExpressions(query)}
                         tableMetadata={tableMetadata}
                         field={breakout}
-                        setField={(field) => this.setBreakout(i, field)}
+                        setField={(field) => this.props.updateQueryBreakout(i, field)}
                         addButton={this.renderAdd(i === 0 ? "Add a grouping" : null)}
                     />
                 );
@@ -315,12 +251,12 @@ export default class GuiQueryEditor extends Component {
                     <DataSelector
                         ref="dataSection"
                         includeTables={true}
-                        query={this.props.query}
+                        datasetQuery={this.props.datasetQuery}
                         databases={this.props.databases}
                         tables={this.props.tables}
                         setDatabaseFn={this.props.setDatabaseFn}
                         setSourceTableFn={this.props.setSourceTableFn}
-                        isInitiallyOpen={(!this.props.query.database || !this.props.query.query.source_table) && !this.props.isShowingTutorial}
+                        isInitiallyOpen={(!this.props.datasetQuery.database || !this.props.datasetQuery.query.source_table) && !this.props.isShowingTutorial}
                     />
                     :
                     <span className="flex align-center px2 py2 text-bold text-grey">
@@ -392,8 +328,8 @@ export default class GuiQueryEditor extends Component {
     }
 
     render() {
-        const { query, databases } = this.props;
-        const readOnly = query.database != null && !_.findWhere(databases, { id: query.database });
+        const { datasetQuery, databases } = this.props;
+        const readOnly = datasetQuery.database != null && !_.findWhere(databases, { id: datasetQuery.database });
         if (readOnly) {
             return <div className="border-bottom border-med" />
         }
@@ -411,7 +347,6 @@ export default class GuiQueryEditor extends Component {
                     {this.props.children}
                     <ExtendedOptions
                         {...this.props}
-                        setQuery={(query) => this.setQuery(query)}
                     />
                 </div>
             </div>
diff --git a/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx b/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
index fea66a4be1249faa432281a28eb3a107791894dc..9c53a89e8aa66c29e8dde0c99f1a3f0f74e1e128 100644
--- a/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
+++ b/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
@@ -40,8 +40,8 @@ import Parameters from "metabase/dashboard/containers/Parameters";
 // *  `mode` :         the ACE Editor mode name, e.g. 'ace/mode/json'
 // *  `description`:   name used to describe the text written in that mode, e.g. 'JSON'. Used to fill in the blank in 'This question is written in _______'.
 // *  `requiresTable`: whether the DB selector should be a DB + Table selector. Mongo needs both DB + Table.
-function getModeInfo(query, databases) {
-    let databaseID = query ? query.database : null,
+function getModeInfo(datasetQuery, databases) {
+    let databaseID = datasetQuery ? datasetQuery.database : null,
         database   = _.findWhere(databases, { id: databaseID }),
         engine     = database ? database.engine : null;
 
@@ -65,13 +65,13 @@ export default class NativeQueryEditor extends Component {
     constructor(props, context) {
         super(props, context);
 
-        const lines = props.query.native.query ?
-            Math.min(MAX_AUTO_SIZE_LINES, countLines(props.query.native.query)) :
+        const lines = props.datasetQuery.native.query ?
+            Math.min(MAX_AUTO_SIZE_LINES, countLines(props.datasetQuery.native.query)) :
             MAX_AUTO_SIZE_LINES;
 
         this.state = {
             showEditor: !(props.card && props.card.id),
-            modeInfo: getModeInfo(props.query, props.databases),
+            modeInfo: getModeInfo(props.datasetQuery, props.databases),
             initialHeight: getEditorLineHeight(lines)
         };
 
@@ -88,9 +88,9 @@ export default class NativeQueryEditor extends Component {
         card: PropTypes.object.isRequired,
         databases: PropTypes.array.isRequired,
         nativeDatabases: PropTypes.array.isRequired,
-        query: PropTypes.object.isRequired,
-        setQueryFn: PropTypes.func.isRequired,
-        runQueryFn: PropTypes.func.isRequired,
+        datasetQuery: PropTypes.object.isRequired,
+        setDatasetQuery: PropTypes.func.isRequired,
+        runQuery: PropTypes.func.isRequired,
         setDatabaseFn: PropTypes.func.isRequired,
         autocompleteResultsFn: PropTypes.func.isRequired,
         isOpen: PropTypes.bool,
@@ -110,9 +110,9 @@ export default class NativeQueryEditor extends Component {
     }
 
     componentWillReceiveProps(nextProps) {
-        if (this.props.query.database !== nextProps.query.database) {
+        if (this.props.datasetQuery.database !== nextProps.datasetQuery.database) {
             this.setState({
-                modeInfo: getModeInfo(nextProps.query, nextProps.databases)
+                modeInfo: getModeInfo(nextProps.datasetQuery, nextProps.databases)
             });
         }
     }
@@ -120,12 +120,12 @@ export default class NativeQueryEditor extends Component {
     componentDidUpdate() {
         const { modeInfo } = this.state;
 
-        if (this._editor.getValue() !== this.props.query.native.query) {
+        if (this._editor.getValue() !== this.props.datasetQuery.native.query) {
             // This is a weird hack, but the purpose is to avoid an infinite loop caused by the fact that calling editor.setValue()
             // will trigger the editor 'change' event, update the query, and cause another rendering loop which we don't want, so
             // we need a way to update the editor without causing the onChange event to go through as well
             this.localUpdate = true;
-            this._editor.setValue(this.props.query.native.query);
+            this._editor.setValue(this.props.datasetQuery.native.query);
             this._editor.clearSelection();
             this.localUpdate = false;
         }
@@ -163,10 +163,10 @@ export default class NativeQueryEditor extends Component {
                 const selectedText = this._editor.getSelectedText();
                 if (selectedText) {
                     const temporaryCard = assocIn(card, ["dataset_query", "native", "query"], selectedText);
-                    this.props.runQueryFn(temporaryCard, false, null, true);
+                    this.props.runQuery(temporaryCard, { shouldUpdateUrl: false });
                 }
             } else {
-                this.props.runQueryFn();
+                this.props.runQuery();
             }
         }
     }
@@ -179,7 +179,7 @@ export default class NativeQueryEditor extends Component {
         this._editor.getSession().on('change', this.onChange);
 
         // initialize the content
-        const querySource = this.props.query.native.query;
+        const querySource = this.props.datasetQuery.native.query;
         this._editor.setValue(querySource);
 
         this._editor.renderer.setScrollMargin(SCROLL_MARGIN, SCROLL_MARGIN);
@@ -240,9 +240,9 @@ export default class NativeQueryEditor extends Component {
     onChange(event) {
         if (this._editor && !this.localUpdate) {
             this._updateSize();
-            const { query } = this.props;
-            if (query.native.query !== this._editor.getValue()) {
-                this.props.setQueryFn(assocIn(query, ["native", "query"], this._editor.getValue()));
+            const { datasetQuery } = this.props;
+            if (datasetQuery.native.query !== this._editor.getValue()) {
+                this.props.setDatasetQuery(assocIn(datasetQuery, ["native", "query"], this._editor.getValue()));
             }
         }
     }
@@ -258,13 +258,13 @@ export default class NativeQueryEditor extends Component {
 
     setTableID(tableID) {
         // translate the table id into the table name
-        let database = this.props.databases ? _.findWhere(this.props.databases, { id: this.props.query.database }) : null,
+        let database = this.props.databases ? _.findWhere(this.props.databases, { id: this.props.datasetQuery.database }) : null,
             table = database ? _.findWhere(database.tables, { id: tableID }) : null;
 
         if (table) {
-            const { query } = this.props;
-            if (query.native.collection !== table.name) {
-                this.props.setQueryFn(assocIn(query, ["native", "collection"], table.name));
+            const { datasetQuery } = this.props;
+            if (datasetQuery.native.collection !== table.name) {
+                this.props.setDatasetQuery(assocIn(datasetQuery, ["native", "collection"], table.name));
             }
         }
     }
@@ -272,18 +272,18 @@ export default class NativeQueryEditor extends Component {
     render() {
         const { parameters, setParameterValue, location } = this.props;
 
-        let modeInfo = getModeInfo(this.props.query, this.props.databases);
+        let modeInfo = getModeInfo(this.props.datasetQuery, this.props.databases);
 
         let dataSelectors = [];
         if (this.state.showEditor && this.props.nativeDatabases) {
             // we only render a db selector if there are actually multiple to choose from
-            if (this.props.nativeDatabases.length > 1 && (this.props.query.database === null || _.any(this.props.nativeDatabases, (db) => db.id === this.props.query.database))) {
+            if (this.props.nativeDatabases.length > 1 && (this.props.datasetQuery.database === null || _.any(this.props.nativeDatabases, (db) => db.id === this.props.datasetQuery.database))) {
                 dataSelectors.push(
                     <div key="db_selector" className="GuiBuilder-section GuiBuilder-data flex align-center">
                         <span className="GuiBuilder-section-label Query-label">Database</span>
                         <DataSelector
                             databases={this.props.nativeDatabases}
-                            query={this.props.query}
+                            datasetQuery={this.props.datasetQuery}
                             setDatabaseFn={this.setDatabaseID}
                         />
                     </div>
@@ -295,10 +295,10 @@ export default class NativeQueryEditor extends Component {
             }
             if (modeInfo.requiresTable) {
                 let databases = this.props.nativeDatabases,
-                    dbId      = this.props.query.database,
+                    dbId      = this.props.datasetQuery.database,
                     database  = databases ? _.findWhere(databases, { id: dbId }) : null,
                     tables    = database ? database.tables : [],
-                    selectedTable = this.props.query.native.collection ? _.findWhere(tables, { name: this.props.query.native.collection }) : null;
+                    selectedTable = this.props.datasetQuery.native.collection ? _.findWhere(tables, { name: this.props.datasetQuery.native.collection }) : null;
 
                 dataSelectors.push(
                     <div key="table_selector" className="GuiBuilder-section GuiBuilder-data flex align-center">
@@ -306,7 +306,7 @@ export default class NativeQueryEditor extends Component {
                         <DataSelector
                             ref="dataSection"
                             includeTables={true}
-                            query={{
+                            datasetQuery={{
                                 type: "query",
                                 query: { source_table: selectedTable ? selectedTable.id : null },
                                 database: dbId
diff --git a/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx b/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx
index 423edf6915dec2f9c7a033a65c78dc7dcc1724cd..337b5ac6b1f0c91c5523244ba746833e0b63483e 100644
--- a/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx
@@ -1,5 +1,4 @@
 import React, { Component, PropTypes } from "react";
-import ReactDOM from "react-dom";
 
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
 import Icon from "metabase/components/Icon.jsx";
@@ -8,7 +7,7 @@ import Tooltip from "metabase/components/Tooltip.jsx";
 
 import FieldSet from "metabase/components/FieldSet.jsx";
 
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import _ from "underscore";
 import cx from "classnames";
@@ -33,13 +32,13 @@ const QueryDownloadWidget = ({ className, card, result, uuid, token }) =>
             <div className="flex flex-row mt2">
                 {["csv", "json"].map(type =>
                     uuid ?
-                        <PublicQueryButton type={type} uuid={uuid} className="mr1 text-uppercase text-default" />
+                        <PublicQueryButton key={type} type={type} uuid={uuid} className="mr1 text-uppercase text-default" />
                     : token ?
-                        <EmbedQueryButton type={type} token={token} className="mr1 text-uppercase text-default" />
+                        <EmbedQueryButton key={type} type={type} token={token} className="mr1 text-uppercase text-default" />
                     : card && card.id ?
-                        <SavedQueryButton type={type} card={card} result={result} className="mr1 text-uppercase text-default" />
+                        <SavedQueryButton key={type} type={type} card={card} result={result} className="mr1 text-uppercase text-default" />
                     : card && !card.id ?
-                        <UnsavedQueryButton type={type} card={card} result={result} className="mr1 text-uppercase text-default" />
+                        <UnsavedQueryButton key={type} type={type} card={card} result={result} className="mr1 text-uppercase text-default" />
                     :
                       null
                 )}
diff --git a/frontend/src/metabase/query_builder/components/QueryHeader.jsx b/frontend/src/metabase/query_builder/components/QueryHeader.jsx
index babeaeeae43a8e9370a83542e2138d0f749f7c80..ff451023ecaa2485dfb2044d9a0cb93533fdc446 100644
--- a/frontend/src/metabase/query_builder/components/QueryHeader.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryHeader.jsx
@@ -23,7 +23,7 @@ import { CardApi, RevisionApi } from "metabase/services";
 import MetabaseAnalytics from "metabase/lib/analytics";
 import Query from "metabase/lib/query";
 import { cancelable } from "metabase/lib/promise";
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import cx from "classnames";
 import _ from "underscore";
@@ -295,7 +295,7 @@ export default class QueryHeader extends Component {
         }
 
         // parameters
-        if (Query.isNative(this.props.query) && database && _.contains(database.features, "native-parameters")) {
+        if (Query.isNative(card && card.dataset_query) && database && _.contains(database.features, "native-parameters")) {
             const parametersButtonClasses = cx('transition-color', {
                 'text-brand': this.props.uiControls.isShowingTemplateTagsEditor,
                 'text-brand-hover': !this.props.uiControls.isShowingTemplateTagsEditor
diff --git a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
index d9e650d7b7814fc5cbe59af9f17cd0537a503ba5..bfa9b9c33acfe617c15b2c9567611c87779b784f 100644
--- a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
@@ -1,8 +1,11 @@
 import React, { Component, PropTypes } from "react";
-import ReactDOM from "react-dom";
 import { Link } from "react-router";
 
 import LoadingSpinner from 'metabase/components/LoadingSpinner.jsx';
+import Tooltip from "metabase/components/Tooltip";
+import Icon from "metabase/components/Icon";
+import ShrinkableList from "metabase/components/ShrinkableList";
+
 import RunButton from './RunButton.jsx';
 import VisualizationSettings from './VisualizationSettings.jsx';
 
@@ -13,18 +16,20 @@ import Warnings from "./Warnings.jsx";
 import QueryDownloadWidget from "./QueryDownloadWidget.jsx";
 import QuestionEmbedWidget from "../containers/QuestionEmbedWidget";
 
-import { formatNumber, inflect } from "metabase/lib/formatting";
+import { formatNumber, inflect, duration } from "metabase/lib/formatting";
 import Utils from "metabase/lib/utils";
 import MetabaseSettings from "metabase/lib/settings";
+import * as Urls from "metabase/lib/urls";
 
 import cx from "classnames";
 import _ from "underscore";
+import moment from "moment";
+
+const REFRESH_TOOLTIP_THRESHOLD = 30 * 1000; // 30 seconds
 
 export default class QueryVisualization extends Component {
     constructor(props, context) {
         super(props, context);
-        this.runQuery = this.runQuery.bind(this);
-
         this.state = this._getStateFromProps(props);
     }
 
@@ -38,13 +43,12 @@ export default class QueryVisualization extends Component {
         setDisplayFn: PropTypes.func.isRequired,
         onUpdateVisualizationSettings: PropTypes.func.isRequired,
         onReplaceAllVisualizationSettings: PropTypes.func.isRequired,
-        setSortFn: PropTypes.func.isRequired,
         cellIsClickableFn: PropTypes.func,
         cellClickedFn: PropTypes.func,
         isRunning: PropTypes.bool.isRequired,
         isRunnable: PropTypes.bool.isRequired,
-        runQueryFn: PropTypes.func.isRequired,
-        cancelQueryFn: PropTypes.func
+        runQuery: PropTypes.func.isRequired,
+        cancelQuery: PropTypes.func
     };
 
     static defaultProps = {
@@ -66,48 +70,85 @@ export default class QueryVisualization extends Component {
         }
     }
 
-    queryIsDirty() {
-        // a query is considered dirty if ANY part of it has been changed
-        return (
-            !Utils.equals(this.props.card.dataset_query, this.state.lastRunDatasetQuery) ||
-            !Utils.equals(this.props.parameterValues, this.state.lastRunParameterValues)
-        );
-    }
-
     isChartDisplay(display) {
         return (display !== "table" && display !== "scalar");
     }
 
-    runQuery() {
-        this.props.runQueryFn();
+    runQuery = () => {
+        this.props.runQuery(null, { ignoreCache: true });
     }
 
     renderHeader() {
-        const { isObjectDetail, isRunning, isAdmin, card, result } = this.props;
-        const isDirty = this.queryIsDirty();
+        const { isObjectDetail, isRunnable, isRunning, isResultDirty, isAdmin, card, result, cancelQuery } = this.props;
         const isSaved = card.id != null;
+
+        let runButtonTooltip;
+        if (!isResultDirty && result && result.cached && result.average_execution_time > REFRESH_TOOLTIP_THRESHOLD) {
+            runButtonTooltip = `This question will take approximately ${duration(result.average_execution_time)} to refresh`;
+        }
+
+        const messages = [];
+        if (result && result.cached) {
+            messages.push({
+                icon: "clock",
+                message: (
+                    <div>
+                        Updated {moment(result.updated_at).fromNow()}
+                    </div>
+                )
+            })
+        }
+        if (result && result.data && !isObjectDetail && card.display === "table") {
+            messages.push({
+                icon: "table2",
+                message: (
+                    <div>
+                        { result.data.rows_truncated != null ? ("Showing first ") : ("Showing ")}
+                        <strong>{formatNumber(result.row_count)}</strong>
+                        { " " + inflect("row", result.data.rows.length) }
+                    </div>
+                )
+            })
+        }
+
         const isPublicLinksEnabled = MetabaseSettings.get("public_sharing");
         const isEmbeddingEnabled = MetabaseSettings.get("embedding");
         return (
-            <div className="relative flex flex-no-shrink mt3 mb1" style={{ minHeight: "2em" }}>
-                <span className="relative z4">
+            <div className="relative flex align-center flex-no-shrink mt2 mb1" style={{ minHeight: "2em" }}>
+                <div className="z4 flex-full">
                   { !isObjectDetail && <VisualizationSettings ref="settings" {...this.props} /> }
-                </span>
-                <div className="absolute flex layout-centered left right z3">
-                    <RunButton
-                        canRun={this.props.isRunnable}
-                        isDirty={isDirty}
-                        isRunning={isRunning}
-                        runFn={this.runQuery}
-                        cancelFn={this.props.cancelQueryFn}
-                    />
                 </div>
-                <div className="absolute right z4 flex align-center" style={{ lineHeight: 0 /* needed to align icons :-/ */ }}>
-                    { !isDirty && this.renderCount() }
+                <div className="z3">
+                    <Tooltip tooltip={runButtonTooltip}>
+                        <RunButton
+                            isRunnable={isRunnable}
+                            isDirty={isResultDirty}
+                            isRunning={isRunning}
+                            onRun={this.runQuery}
+                            onCancel={cancelQuery}
+                        />
+                    </Tooltip>
+                </div>
+                <div className="z4 flex-full flex align-center justify-end" style={{ lineHeight: 0 /* needed to align icons :-/ */ }}>
+                    <ShrinkableList
+                        className="flex"
+                        items={messages}
+                        renderItem={(item) =>
+                            <div className="flex-no-shrink flex align-center mx2 h5 text-grey-4">
+                                <Icon className="mr1" name={item.icon} size={12} />
+                                {item.message}
+                            </div>
+                        }
+                        renderItemSmall={(item) =>
+                            <Tooltip tooltip={<div className="p1">{item.message}</div>}>
+                                <Icon className="mx1" name={item.icon} size={16} />
+                            </Tooltip>
+                        }
+                    />
                     { !isObjectDetail &&
-                        <Warnings warnings={this.state.warnings} className="mx2" size={18} />
+                        <Warnings warnings={this.state.warnings} className="mx1" size={18} />
                     }
-                    { !isDirty && result && !result.error ?
+                    { !isResultDirty && result && !result.error ?
                         <QueryDownloadWidget
                             className="mx1"
                             card={card}
@@ -128,21 +169,8 @@ export default class QueryVisualization extends Component {
         );
     }
 
-    renderCount() {
-        let { result, isObjectDetail, card } = this.props;
-        if (result && result.data && !isObjectDetail && card.display === "table") {
-            return (
-                <div>
-                    { result.data.rows_truncated != null ? ("Showing first ") : ("Showing ")}
-                    <b>{formatNumber(result.row_count)}</b>
-                    { " " + inflect("row", result.data.rows.length) }.
-                </div>
-            );
-        }
-    }
-
     render() {
-        const { card, databases, isObjectDetail, isRunning, result } = this.props
+        const { className, card, databases, isObjectDetail, isRunning, result } = this.props
         let viz;
 
         if (!result) {
@@ -160,24 +188,25 @@ export default class QueryVisualization extends Component {
                         onUpdateWarnings={(warnings) => this.setState({ warnings })}
                         onOpenChartSettings={() => this.refs.settings.open()}
                         {...this.props}
+                        className="spread"
                     />
                 );
             }
         }
 
-        const wrapperClasses = cx('wrapper full relative mb2 z1', {
+        const wrapperClasses = cx(className, 'relative', {
             'flex': !isObjectDetail,
             'flex-column': !isObjectDetail
         });
 
-        const visualizationClasses = cx('flex flex-full Visualization z1 px1', {
+        const visualizationClasses = cx('flex flex-full Visualization z1 relative', {
             'Visualization--errors': (result && result.error),
             'Visualization--loading': isRunning
         });
 
         return (
             <div className={wrapperClasses}>
-                {this.renderHeader()}
+                { !this.props.noHeader && this.renderHeader()}
                 { isRunning && (
                     <div className="Loading spread flex flex-column layout-centered text-brand z2">
                         <LoadingSpinner />
@@ -195,5 +224,5 @@ export default class QueryVisualization extends Component {
 const VisualizationEmptyState = ({showTutorialLink}) =>
     <div className="flex full layout-centered text-grey-1 flex-column">
         <h1>If you give me some data I can show you something cool. Run a Query!</h1>
-        { showTutorialLink && <Link to="/q#?tutorial" className="link cursor-pointer my2">How do I use this thing?</Link> }
+        { showTutorialLink && <Link to={Urls.question(null, "?tutorial")} className="link cursor-pointer my2">How do I use this thing?</Link> }
     </div>
diff --git a/frontend/src/metabase/query_builder/components/QuickFilterPopover.jsx b/frontend/src/metabase/query_builder/components/QuickFilterPopover.jsx
deleted file mode 100644
index d63128f85631c629dddf72f11726d73a3764ac57..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/query_builder/components/QuickFilterPopover.jsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import React from "react";
-
-import Popover from "metabase/components/Popover.jsx";
-import { TYPE, isa } from "metabase/lib/types";
-
-function getFiltersForColumn(column) {
-    if (isa(column.base_type, TYPE.Number) || isa(column.base_type, TYPE.DateTime)) {
-        return [
-            { name: "<", value: "<" },
-            { name: "=", value: "=" },
-            { name: "≠", value: "!=" },
-            { name: ">", value: ">" }
-        ];
-    } else {
-        return [
-            { name: "=", value: "=" },
-            { name: "≠", value: "!=" }
-        ];
-    }
-}
-
-const QuickFilterPopover = ({ onFilter, onClose, column }) =>
-    <Popover
-        hasArrow={false}
-        tetherOptions={{
-            targetAttachment: "middle center",
-            attachment: "middle center"
-        }}
-        onClose={onClose}
-    >
-        <div className="bg-white bordered shadowed p1">
-            <ul className="h1 flex align-center">
-                { getFiltersForColumn(column).map(({ name, value }) =>
-                    <li key={value} className="p2 text-brand-hover" onClick={() => onFilter(value)}>{name}</li>
-                )}
-            </ul>
-        </div>
-    </Popover>
-
-export default QuickFilterPopover;
diff --git a/frontend/src/metabase/query_builder/components/RunButton.jsx b/frontend/src/metabase/query_builder/components/RunButton.jsx
index 1fd61bdc9968c519ec9ea95838c38180581dfb28..64763834590a41f8a9f0417bb436ffa28ab4793f 100644
--- a/frontend/src/metabase/query_builder/components/RunButton.jsx
+++ b/frontend/src/metabase/query_builder/components/RunButton.jsx
@@ -6,24 +6,24 @@ import cx from "classnames";
 
 export default class RunButton extends Component {
     static propTypes = {
-        canRun: PropTypes.bool.isRequired,
+        isRunnable: PropTypes.bool.isRequired,
         isRunning: PropTypes.bool.isRequired,
         isDirty: PropTypes.bool.isRequired,
-        runFn: PropTypes.func.isRequired,
-        cancelFn: PropTypes.func
+        onRun: PropTypes.func.isRequired,
+        onCancel: PropTypes.func
     };
 
     render() {
-        let { canRun, isRunning, isDirty, runFn, cancelFn } = this.props;
+        let { isRunnable, isRunning, isDirty, onRun, onCancel } = this.props;
         let buttonText = null;
         if (isRunning) {
             buttonText = <div className="flex align-center"><Icon className="mr1" name="close" />Cancel</div>;
-        } else if (canRun && isDirty) {
+        } else if (isRunnable && isDirty) {
             buttonText = "Get Answer";
-        } else if (canRun && !isDirty) {
+        } else if (isRunnable && !isDirty) {
             buttonText = <div className="flex align-center"><Icon className="mr1" name="refresh" />Refresh</div>;
         }
-        let actionFn = isRunning ? cancelFn : runFn;
+        let actionFn = isRunning ? onCancel : onRun;
         let classes = cx("Button Button--medium circular RunButton", {
             "RunButton--hidden": !buttonText,
             "Button--primary": isDirty,
@@ -31,7 +31,7 @@ export default class RunButton extends Component {
             "text-grey-4-hover": !isDirty,
         });
         return (
-            <button className={classes} onClick={actionFn}>
+            <button className={classes} onClick={() => actionFn()}>
             {buttonText}
             </button>
         );
diff --git a/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx b/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx
index 3d120352d8ea388e4a438422cd24ab7ece413ace..029ebb2228be2814afc33f55c52bb78ecf0c7d2e 100644
--- a/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx
+++ b/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx
@@ -32,11 +32,11 @@ export default class TimeGroupingPopover extends Component {
 
     static propTypes = {
         field: PropTypes.oneOfType([PropTypes.number, PropTypes.array]),
-        value: PropTypes.oneOfType([PropTypes.number, PropTypes.array]),
         onFieldChange: PropTypes.func.isRequired
     };
 
     static defaultProps = {
+        title: "Group time by",
         groupingOptions: [
             // "default",
             "minute",
@@ -58,15 +58,17 @@ export default class TimeGroupingPopover extends Component {
     }
 
     setField(bucketing) {
-        this.props.onFieldChange(["datetime-field", this.props.value, "as", bucketing]);
+        this.props.onFieldChange(["datetime-field", this.props.field[1], "as", bucketing]);
     }
 
     render() {
-        const { field } = this.props;
-        const enabledOptions = new Set(this.props.groupingOptions);
+        const { title, field, className, groupingOptions } = this.props;
+        const enabledOptions = new Set(groupingOptions);
         return (
-            <div className="px2 pt2 pb1" style={{width:"250px"}}>
-                <h3 className="List-section-header mx2">Group time by</h3>
+            <div className={cx(className, "px2 py1")} style={{width:"250px"}}>
+                { title &&
+                    <h3 className="List-section-header pt1 mx2">{title}</h3>
+                }
                 <ul className="py1">
                 { BUCKETINGS.filter(o => o == null || enabledOptions.has(o)).map((bucketing, bucketingIndex) =>
                     bucketing == null ?
diff --git a/frontend/src/metabase/query_builder/components/VisualizationResult.jsx b/frontend/src/metabase/query_builder/components/VisualizationResult.jsx
index 68f07ebcac246980e0dfc2a3b90a3cd038cf8ebb..1637825a4044fe8e1739dfe5856f6b1d6f0c711d 100644
--- a/frontend/src/metabase/query_builder/components/VisualizationResult.jsx
+++ b/frontend/src/metabase/query_builder/components/VisualizationResult.jsx
@@ -4,11 +4,14 @@ import React, { PropTypes } from "react";
 import QueryVisualizationObjectDetailTable from './QueryVisualizationObjectDetailTable.jsx';
 import VisualizationErrorMessage from './VisualizationErrorMessage';
 import Visualization from "metabase/visualizations/components/Visualization.jsx";
+import { datasetContainsNoResults } from "metabase/lib/dataset";
+
+const VisualizationResult = ({card, isObjectDetail, lastRunDatasetQuery, result, ...props}) => {
+    const noResults = datasetContainsNoResults(result.data);
 
-const VisualizationResult = ({card, isObjectDetail, lastRunDatasetQuery, result, ...rest}) => {
     if (isObjectDetail) {
-        return <QueryVisualizationObjectDetailTable data={result.data} {...rest} />
-    } else if (result.data.rows.length === 0) {
+        return <QueryVisualizationObjectDetailTable data={result.data} {...props} />
+    } else if (noResults) {
         // successful query but there were 0 rows returned with the result
         return <VisualizationErrorMessage
                   type='noRows'
@@ -29,11 +32,11 @@ const VisualizationResult = ({card, isObjectDetail, lastRunDatasetQuery, result,
             dataset_query: lastRunDatasetQuery
         };
         return <Visualization
-                  className="full"
                   series={[{ card: vizCard, data: result.data }]}
+                  onChangeCardAndRun={props.setCardAndRun}
                   isEditing={true}
                   // Table:
-                  {...rest}
+                  {...props}
               />
     }
 }
@@ -43,6 +46,7 @@ VisualizationResult.propTypes = {
     isObjectDetail:         PropTypes.bool.isRequired,
     lastRunDatasetQuery:    PropTypes.object.isRequired,
     result:                 PropTypes.object.isRequired,
+    setCardAndRun:          PropTypes.func,
 }
 
 export default VisualizationResult;
diff --git a/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx b/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx
index 0ec813278707e512880b186cc7fcf6ac9623ce83..31c4a8b5638d6a4e228ad55a2f24ad5761b5fd3c 100644
--- a/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx
@@ -26,8 +26,8 @@ export default class DataReference extends Component {
     static propTypes = {
         query: PropTypes.object.isRequired,
         onClose: PropTypes.func.isRequired,
-        runQueryFn: PropTypes.func.isRequired,
-        setQueryFn: PropTypes.func.isRequired,
+        runQuery: PropTypes.func.isRequired,
+        setDatasetQuery: PropTypes.func.isRequired,
         setDatabaseFn: PropTypes.func.isRequired,
         setSourceTableFn: PropTypes.func.isRequired,
         setDisplayFn: PropTypes.func.isRequired
diff --git a/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx b/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx
index c720a8e3af291dc208595120fed6d873f8e9c1b6..3e301ac5e2676b8620043091f2409bcfa13ca8d8 100644
--- a/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx
@@ -26,10 +26,10 @@ export default class FieldPane extends Component {
 
     static propTypes = {
         field: PropTypes.object.isRequired,
-        query: PropTypes.object,
+        datasetQuery: PropTypes.object,
         loadTableAndForeignKeysFn: PropTypes.func.isRequired,
-        runQueryFn: PropTypes.func.isRequired,
-        setQueryFn: PropTypes.func.isRequired,
+        runQuery: PropTypes.func.isRequired,
+        setDatasetQuery: PropTypes.func.isRequired,
         setCardAndRun: PropTypes.func.isRequired
     };
 
@@ -47,23 +47,23 @@ export default class FieldPane extends Component {
     }
 
     filterBy() {
-        var query = this.setDatabaseAndTable();
+        var datasetQuery = this.setDatabaseAndTable();
         // Add an aggregation so both aggregation and filter popovers aren't visible
-        if (!Query.hasValidAggregation(query.query)) {
-            Query.clearAggregations(query.query);
+        if (!Query.hasValidAggregation(datasetQuery.query)) {
+            Query.clearAggregations(datasetQuery.query);
         }
-        Query.addFilter(query.query, [null, this.props.field.id, null]);
-        this.props.setQueryFn(query);
+        Query.addFilter(datasetQuery.query, [null, this.props.field.id, null]);
+        this.props.setDatasetQuery(datasetQuery);
     }
 
     groupBy() {
-        let query = this.props.query;
-        if (!Query.hasValidAggregation(query.query)) {
-            Query.clearAggregations(query.query);
+        let { datasetQuery } = this.props;
+        if (!Query.hasValidAggregation(datasetQuery.query)) {
+            Query.clearAggregations(datasetQuery.query);
         }
-        Query.addBreakout(query.query, this.props.field.id);
-        this.props.setQueryFn(query);
-        this.props.runQueryFn();
+        Query.addBreakout(datasetQuery.query, this.props.field.id);
+        this.props.setDatasetQuery(datasetQuery);
+        this.props.runQuery();
     }
 
     newCard() {
@@ -94,7 +94,7 @@ export default class FieldPane extends Component {
     }
 
     render() {
-        let { field, query } = this.props;
+        let { field, datasetQuery } = this.props;
         let { table, error } = this.state;
 
         let fieldName = field.display_name;
@@ -111,12 +111,12 @@ export default class FieldPane extends Component {
         }
 
         // TODO: allow for filters/grouping via foreign keys
-        if (!query.query || query.query.source_table == undefined || query.query.source_table === field.table_id) {
+        if (!datasetQuery.query || datasetQuery.query.source_table == undefined || datasetQuery.query.source_table === field.table_id) {
             // NOTE: disabled this for now because we need a way to capture the completed filter before adding it to the query, or to pop open the filter widget here?
             // useForCurrentQuestion.push(<UseForButton title={"Filter by " + name} onClick={this.filterBy} />);
 
             // current field must be a valid breakout option for this table AND cannot already be in the breakout clause of our query
-            if (validBreakout && this.state.table.id === this.props.query.query.source_table && (query.query.breakout && !_.contains(query.query.breakout, field.id))) {
+            if (validBreakout && this.state.table.id === datasetQuery.query.source_table && (datasetQuery.query.breakout && !_.contains(datasetQuery.query.breakout, field.id))) {
                 useForCurrentQuestion.push(<UseForButton title={"Group by " + name} onClick={this.groupBy} />);
             }
         }
diff --git a/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx b/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx
index 905bf5f8ccf8993b0d322528af99e1cb247f26f6..fc2d964519f992d7a86744ac048827468efe2fd7 100644
--- a/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx
@@ -26,8 +26,8 @@ export default class MetricPane extends Component {
         metric: PropTypes.object.isRequired,
         query: PropTypes.object,
         loadTableAndForeignKeysFn: PropTypes.func.isRequired,
-        runQueryFn: PropTypes.func.isRequired,
-        setQueryFn: PropTypes.func.isRequired,
+        runQuery: PropTypes.func.isRequired,
+        setDatasetQuery: PropTypes.func.isRequired,
         setCardAndRun: PropTypes.func.isRequired
     };
 
diff --git a/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx b/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx
index 1ca5be667f8fb63c49cb15e25c9fa52b4fc94e6f..0e3a9f4d32a23619ce3fbffa6781c33db8013ed9 100644
--- a/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx
@@ -25,10 +25,10 @@ export default class SegmentPane extends Component {
 
     static propTypes = {
         segment: PropTypes.object.isRequired,
-        query: PropTypes.object,
+        datasetQuery: PropTypes.object,
         loadTableAndForeignKeysFn: PropTypes.func.isRequired,
-        runQueryFn: PropTypes.func.isRequired,
-        setQueryFn: PropTypes.func.isRequired,
+        runQuery: PropTypes.func.isRequired,
+        setDatasetQuery: PropTypes.func.isRequired,
         setCardAndRun: PropTypes.func.isRequired
     };
 
@@ -46,14 +46,14 @@ export default class SegmentPane extends Component {
     }
 
     filterBy() {
-        let query = this.props.query;
+        let { datasetQuery } = this.props;
         // Add an aggregation so both aggregation and filter popovers aren't visible
-        if (!Query.hasValidAggregation(query.query)) {
-            Query.clearAggregations(query.query);
+        if (!Query.hasValidAggregation(datasetQuery.query)) {
+            Query.clearAggregations(datasetQuery.query);
         }
-        Query.addFilter(query.query, ["SEGMENT", this.props.segment.id]);
-        this.props.setQueryFn(query);
-        this.props.runQueryFn();
+        Query.addFilter(datasetQuery.query, ["SEGMENT", this.props.segment.id]);
+        this.props.setDatasetQuery(datasetQuery);
+        this.props.runQuery();
     }
 
     newCard() {
@@ -77,7 +77,7 @@ export default class SegmentPane extends Component {
     }
 
     render() {
-        let { segment, query } = this.props;
+        let { segment, datasetQuery } = this.props;
         let { error, table } = this.state;
 
         let segmentName = segment.name;
@@ -85,7 +85,7 @@ export default class SegmentPane extends Component {
         let useForCurrentQuestion = [];
         let usefulQuestions = [];
 
-        if (query.query && query.query.source_table === segment.table_id && !_.findWhere(Query.getFilters(query.query), { [0]: "SEGMENT", [1]: segment.id })) {
+        if (datasetQuery.query && datasetQuery.query.source_table === segment.table_id && !_.findWhere(Query.getFilters(datasetQuery.query), { [0]: "SEGMENT", [1]: segment.id })) {
             useForCurrentQuestion.push(<UseForButton title={"Filter by " + segmentName} onClick={this.filterBy} />);
         }
 
diff --git a/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx b/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx
index cb82754cbc81b3d0c15d77a81d6314887069492d..16eb3999fac7140f2e4c81a173ca548a267082a9 100644
--- a/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx
@@ -1,7 +1,7 @@
 /* @flow */
 
 import React, { Component, PropTypes } from "react";
-import ReactCSSTransitionGroup from "react-addons-css-transition-group";
+
 import cx from "classnames";
 import { titleCase } from "humanize-plus";
 
@@ -14,7 +14,8 @@ type Operator = {
 type Props = {
     operator: string,
     operators: Operator[],
-    onOperatorChange: (o: Operator) => void
+    onOperatorChange: (o: Operator) => void,
+    hideTimeSelectors?: bool
 }
 
 type State = {
diff --git a/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx b/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx
index 7aadb04e7e3fe1ec49d7e9abf6329d809b42fb43..1f3460474b824cbce14b121188d7b1c930c2a0bd 100644
--- a/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx
@@ -22,7 +22,6 @@ import type { FieldFilter, ConcreteField, ExpressionClause } from "metabase/meta
 import type { TableMetadata, FieldMetadata, Operator } from "metabase/meta/types/Metadata";
 
 type Props = {
-    isNew?: bool,
     filter?: FieldFilter,
     onCommitFilter: () => void,
     onClose: () => void,
@@ -43,12 +42,11 @@ export default class FilterPopover extends Component<*, Props, State> {
 
         this.state = {
             // $FlowFixMe
-            filter: (props.isNew ? [] : props.filter)
+            filter: props.filter || []
         };
     }
 
     static propTypes = {
-        isNew: PropTypes.bool,
         filter: PropTypes.array,
         onCommitFilter: PropTypes.func.isRequired,
         onClose: PropTypes.func.isRequired,
@@ -273,10 +271,9 @@ export default class FilterPopover extends Component<*, Props, State> {
                     </div>
                     { isDate(field) ?
                         <DatePicker
+                            className="mt1 border-top"
                             filter={filter}
                             onFilterChange={this.setFilter}
-                            onOperatorChange={this.setOperator}
-                            tableMetadata={this.props.tableMetadata}
                         />
                     :
                         <div>
@@ -294,7 +291,7 @@ export default class FilterPopover extends Component<*, Props, State> {
                             className={cx("Button Button--purple full", { "disabled": !this.isValid() })}
                             onClick={() => this.commitFilter(this.state.filter)}
                         >
-                            {this.props.isNew ? "Add filter" : "Update filter"}
+                            {!this.props.filter ? "Add filter" : "Update filter"}
                         </button>
                     </div>
                 </div>
@@ -302,11 +299,3 @@ export default class FilterPopover extends Component<*, Props, State> {
         }
     }
 }
-
-FilterPopover.propTypes = {
-    tableMetadata: PropTypes.object.isRequired,
-    isNew: PropTypes.bool,
-    filter: PropTypes.array,
-    onCommitFilter: PropTypes.func.isRequired,
-    onClose: PropTypes.func.isRequired
-};
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx
index 4ae6fe3fe3eeadfcadcec666b6b3967e69e75a14..bfb8e9cd2d56f47d3a05674c6a7435c0a4401392 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx
@@ -3,7 +3,7 @@
 import React, { Component, PropTypes } from "react";
 
 import SpecificDatePicker from "./SpecificDatePicker";
-import RelativeDatePicker, { UnitPicker } from "./RelativeDatePicker";
+import RelativeDatePicker, { DATE_PERIODS, UnitPicker } from "./RelativeDatePicker";
 import DateOperatorSelector from "../DateOperatorSelector";
 import Calendar from "metabase/components/Calendar";
 
@@ -11,6 +11,7 @@ import moment from "moment";
 
 import Query from "metabase/lib/query";
 import { mbqlEq } from "metabase/lib/query/util";
+import cx from "classnames";
 
 import _ from "underscore";
 
@@ -21,15 +22,25 @@ import type {
     LocalFieldReference, ForeignFieldReference, ExpressionReference
 } from "metabase/meta/types/Query";
 
-const SingleDatePicker = ({ filter: [op, field, value], onFilterChange }) =>
-    <SpecificDatePicker value={value} onChange={(value) => onFilterChange([op, field, value])} calendar />
+const SingleDatePicker = ({ filter: [op, field, value], onFilterChange, hideTimeSelectors }) =>
+    <SpecificDatePicker
+        value={value}
+        onChange={(value) => onFilterChange([op, field, value])}
+        hideTimeSelectors={hideTimeSelectors}
+        calendar />
 
-const MultiDatePicker = ({ filter: [op, field, startValue, endValue], onFilterChange }) =>
+const MultiDatePicker = ({ filter: [op, field, startValue, endValue], onFilterChange , hideTimeSelectors}) =>
     <div className="mx2 mb1">
         <div className="flex">
-            <SpecificDatePicker value={startValue} onChange={(value) => onFilterChange([op, field, value, endValue])}  />
+            <SpecificDatePicker
+                value={startValue}
+                hideTimeSelectors={hideTimeSelectors}
+                onChange={(value) => onFilterChange([op, field, value, endValue])}  />
             <span className="mx2 mt2">&ndash;</span>
-            <SpecificDatePicker value={endValue} onChange={(value) => onFilterChange([op, field, startValue, value])} />
+            <SpecificDatePicker
+                value={endValue}
+                hideTimeSelectors={hideTimeSelectors}
+                onChange={(value) => onFilterChange([op, field, startValue, value])} />
         </div>
         <div className="Calendar--noContext">
             <Calendar
@@ -48,8 +59,7 @@ const PreviousPicker =  (props) =>
 const NextPicker = (props) =>
     <RelativeDatePicker {...props} />
 
-
-type CurentPickerProps = {
+type CurrentPickerProps = {
     filter: TimeIntervalFilter,
     onFilterChange: (filter: TimeIntervalFilter) => void
 };
@@ -58,8 +68,8 @@ type CurrentPickerState = {
     showUnits: boolean
 };
 
-class CurrentPicker extends Component<*, CurentPickerProps, CurrentPickerState> {
-    props: CurentPickerProps;
+class CurrentPicker extends Component<*, CurrentPickerProps, CurrentPickerState> {
+    props: CurrentPickerProps;
     state: CurrentPickerState;
 
     constructor(props) {
@@ -79,7 +89,8 @@ class CurrentPicker extends Component<*, CurentPickerProps, CurrentPickerState>
                         this.setState({ showUnits: false });
                     }}
                     togglePicker={() => this.setState({ showUnits: !this.state.showUnits })}
-                    formatter={(val) => val }
+                    formatter={(val) => val}
+                    periods={DATE_PERIODS}
                 />
             </div>
         )
@@ -120,22 +131,26 @@ function getDateTimeFieldTarget(field: ConcreteField): LocalFieldReference|Forei
 }
 
 // wraps values in "datetime-field" is any of them have a time component
-function getDateTimeFieldAndValues(filter: FieldFilter): [ConcreteField, any] {
-    const values = filter.slice(2).map(value => value && getDate(value));
+function getDateTimeFieldAndValues(filter: FieldFilter, count: number): [ConcreteField, any] {
+    const values = filter.slice(2, 2 + count).map(value => value && getDate(value));
     const bucketing = _.any(values, hasTime) ? "minute" : null;
     const field = getDateTimeField(filter[1], bucketing);
     // $FlowFixMe
     return [field, ...values];
 }
 
+
+export type OperatorName =
+    ("Previous"|"Next"|"Current"|"Before"|"After"|"On"|"Between"|"Is Empty"|"Not Empty");
+
 export type Operator = {
-    name: string,
+    name: OperatorName,
     widget?: any,
     init: (filter: FieldFilter) => any,
     test: (filter: FieldFilter) => boolean
 }
 
-const OPERATORS: Operator[] = [
+export const DATE_OPERATORS: Operator[] = [
     {
         name: "Previous",
         init: (filter) => ["time-interval", getDateTimeField(filter[1]), -getIntervals(filter), getUnit(filter)],
@@ -158,28 +173,32 @@ const OPERATORS: Operator[] = [
     },
     {
         name: "Before",
-        init: (filter) =>  ["<", ...getDateTimeFieldAndValues(filter)],
+        init: (filter) =>  ["<", ...getDateTimeFieldAndValues(filter, 1)],
         test: ([op]) => op === "<",
         widget: SingleDatePicker,
     },
     {
         name: "After",
-        init: (filter) => [">", ...getDateTimeFieldAndValues(filter)],
+        init: (filter) => [">", ...getDateTimeFieldAndValues(filter, 1)],
         test: ([op]) => op === ">",
         widget: SingleDatePicker,
     },
     {
         name: "On",
-        init: (filter) => ["=", ...getDateTimeFieldAndValues(filter)],
+        init: (filter) => ["=", ...getDateTimeFieldAndValues(filter, 1)],
         test: ([op]) => op === "=",
         widget: SingleDatePicker,
     },
     {
         name: "Between",
-        init: (filter) => ["BETWEEN", ...getDateTimeFieldAndValues(filter)],
-        test: ([op]) => op === "BETWEEN",
+        init: (filter) => ["BETWEEN", ...getDateTimeFieldAndValues(filter, 2)],
+        test: ([op]) => mbqlEq(op, "between"),
         widget: MultiDatePicker,
     },
+
+];
+
+export const EMPTINESS_OPERATORS: Operator[] = [
     {
         name: "Is Empty",
         init: (filter) => ["IS_NULL", getDateTimeField(filter[1])],
@@ -192,44 +211,56 @@ const OPERATORS: Operator[] = [
     }
 ];
 
+export const ALL_OPERATORS: Operator[] = DATE_OPERATORS.concat(EMPTINESS_OPERATORS);
+
 type Props = {
+    className?: string,
     filter: FieldFilter,
     onFilterChange: (filter: FieldFilter) => void,
-    tableMetadata: any
+    className: ?string,
+    hideEmptinessOperators: boolean, // Don't show is empty / not empty dialog
+    hideTimeSelectors?: boolean
 }
 
 export default class DatePicker extends Component<*, Props, *> {
     static propTypes = {
         filter: PropTypes.array.isRequired,
         onFilterChange: PropTypes.func.isRequired,
-        tableMetadata: PropTypes.object.isRequired
+        className: PropTypes.string,
+        hideEmptinessOperators: PropTypes.bool,
+        hideTimeSelectors: PropTypes.bool
     };
 
     componentWillMount() {
-        const operator = this._getOperator() || OPERATORS[0];
+        const operators = this.props.hideEmptinessOperators ? DATE_OPERATORS : ALL_OPERATORS;
+
+        const operator = this._getOperator(operators) || operators[0];
         this.props.onFilterChange(operator.init(this.props.filter));
+
+        this.setState({operators})
     }
 
-    _getOperator() {
-        return _.find(OPERATORS, (o) => o.test(this.props.filter));
+    _getOperator(operators: Operator[]) {
+        return _.find(operators, (o) => o.test(this.props.filter));
     }
 
     render() {
-        let { filter, onFilterChange } = this.props;
-        const operator = this._getOperator();
+        let { filter, onFilterChange, className} = this.props;
+        const operator = this._getOperator(this.state.operators);
         const Widget = operator && operator.widget;
 
         return (
-            <div className="mt1 pt2 border-top">
+            <div className={cx("pt2", className)}>
                 <DateOperatorSelector
                     operator={operator && operator.name}
-                    operators={OPERATORS}
+                    operators={this.state.operators}
                     onOperatorChange={operator => onFilterChange(operator.init(filter))}
                 />
                 { Widget &&
                     <Widget
                         {...this.props}
                         filter={filter}
+                        hideHoursAndMinutes={this.props.hideTimeSelectors}
                         onFilterChange={filter => {
                             if (operator && operator.init) {
                                 onFilterChange(operator.init(filter));
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx
index 204594f76215c21ba2319f6c856d8f58fbd6a7a6..910842392f55fd9be4199b399a2d49270cf91be0 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx
@@ -23,7 +23,13 @@ export default class NumberPicker extends Component<*, Props, State> {
     constructor(props: Props) {
         super(props);
         this.state = {
-            stringValues: props.values.map(v => String(v || "")),
+            stringValues: props.values.map(v => {
+                if(typeof v === 'number') {
+                    return String(v)
+                } else {
+                    return String(v || "")
+                }
+            }),
             validations: this._validate(props.values)
         }
     }
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx
index 27deeff71159f1a9f957e92a54e032dbfe5daaad..589a529dc9c10ad0a879c10f09ca1b6ac84d7399 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx
@@ -9,20 +9,25 @@ import NumericInput from "./NumericInput.jsx";
 
 import type { TimeIntervalFilter, RelativeDatetimeUnit } from "metabase/meta/types/Query";
 
-const PERIODS: RelativeDatetimeUnit[] = [
-    "minute",
-    "hour",
+export const DATE_PERIODS: RelativeDatetimeUnit[] = [
     "day",
     "week",
     "month",
     "year"
 ];
 
+const TIME_PERIODS: RelativeDatetimeUnit[] = [
+    "minute",
+    "hour",
+];
+
+const ALL_PERIODS = DATE_PERIODS.concat(TIME_PERIODS);
 
 type Props = {
     filter: TimeIntervalFilter,
     onFilterChange: (filter: TimeIntervalFilter) => void,
-    formatter: (value: any) => any
+    formatter: (value: any) => any,
+    hideTimeSelectors?: boolean
 }
 
 type State = {
@@ -43,7 +48,8 @@ export default class RelativeDatePicker extends Component<*, Props, State> {
     static propTypes = {
         filter: PropTypes.array.isRequired,
         onFilterChange: PropTypes.func.isRequired,
-        formatter: PropTypes.func.isRequired
+        formatter: PropTypes.func.isRequired,
+        hideTimeSelectors: PropTypes.bool
     };
 
     static defaultProps = {
@@ -74,6 +80,7 @@ export default class RelativeDatePicker extends Component<*, Props, State> {
                     // $FlowFixMe: intervals could be a string like "current" "next"
                     intervals={intervals}
                     formatter={formatter}
+                    periods={this.props.hideTimeSelectors ? DATE_PERIODS : ALL_PERIODS}
                 />
             </div>
         );
@@ -86,10 +93,11 @@ type UnitPickerProps = {
     open: bool,
     intervals?: number,
     togglePicker: () => void,
-    formatter: (value: ?number) => ?number
+    formatter: (value: ?number) => ?number,
+    periods: RelativeDatetimeUnit[]
 }
 
-export const UnitPicker = ({ open, value, onChange, togglePicker, intervals, formatter }: UnitPickerProps) =>
+export const UnitPicker = ({ open, value, onChange, togglePicker, intervals, formatter, periods }: UnitPickerProps) =>
    <div>
        <div
            onClick={() => togglePicker()}
@@ -110,7 +118,7 @@ export const UnitPicker = ({ open, value, onChange, togglePicker, intervals, for
                 overflow: 'hidden'
             }}
         >
-           { PERIODS.map((unit, index) =>
+           { periods.map((unit, index) =>
                <li
                    className={cx(
                        'List-item cursor-pointer p1',
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx
index 7023ee68bd6e969b067e84cb738c2866510e4d90..8b5096629906e92327da0907e198583a51227298 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx
@@ -18,7 +18,8 @@ const DATE_TIME_FORMAT = "YYYY-MM-DDTHH:mm:ss";
 type Props = {
     value: ?string,
     onChange: (value: ?string) => void,
-    calendar?: bool
+    calendar?: bool,
+    hideTimeSelectors?: bool
 }
 
 type State = {
@@ -66,7 +67,7 @@ export default class SpecificDatePicker extends Component<*, Props, State> {
     }
 
     render() {
-        const { value, calendar } = this.props;
+        const { value, calendar, hideTimeSelectors } = this.props;
         const { showCalendar } = this.state;
 
         let date, hours, minutes;
@@ -130,28 +131,30 @@ export default class SpecificDatePicker extends Component<*, Props, State> {
                     </ExpandingContent>
                 }
 
-                <div className={cx({ 'py2': calendar }, { 'mb3': !calendar })}>
-                    { hours == null || minutes == null ?
-                        <div
-                            className="text-purple-hover cursor-pointer flex align-center"
-                            onClick={() => this.onChange(date, 12, 30) }
-                        >
-                            <Icon
-                                className="mr1"
-                                name='clock'
+                { !hideTimeSelectors &&
+                    <div className={cx({'py2': calendar}, {'mb3': !calendar})}>
+                        { hours == null || minutes == null ?
+                            <div
+                                className="text-purple-hover cursor-pointer flex align-center"
+                                onClick={() => this.onChange(date, 12, 30) }
+                            >
+                                <Icon
+                                    className="mr1"
+                                    name='clock'
+                                />
+                                Add a time
+                            </div>
+                            :
+                            <HoursMinutes
+                                clear={() => this.onChange(date, null, null)}
+                                hours={hours}
+                                minutes={minutes}
+                                onChangeHours={hours => this.onChange(date, hours, minutes)}
+                                onChangeMinutes={minutes => this.onChange(date, hours, minutes)}
                             />
-                            Add a time
-                        </div>
-                    :
-                        <HoursMinutes
-                            clear={() => this.onChange(date, null, null)}
-                            hours={hours}
-                            minutes={minutes}
-                            onChangeHours={hours => this.onChange(date, hours, minutes)}
-                            onChangeMinutes={minutes => this.onChange(date, hours, minutes)}
-                        />
-                    }
-                </div>
+                        }
+                    </div>
+                }
             </div>
         )
     }
diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx
index 153bb3b5376e0efda390eeb26f1ce1c314bb4b17..4a43f13bf97862f1d7cadfc605e03e010a09064e 100644
--- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx
+++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx
@@ -48,16 +48,16 @@ const EXAMPLES = {
 }
 
 
-const TagExample = ({ datasetQuery, setQuery }) =>
+const TagExample = ({ datasetQuery, setDatasetQuery }) =>
     <div>
         <h5>Example:</h5>
         <p>
             <Code>{datasetQuery.native.query}</Code>
-            { setQuery && (
+            { setDatasetQuery && (
                 <div
                     className="Button Button--small"
                     data-metabase-event="QueryBuilder;Template Tag Example Query Used"
-                    onClick={() => setQuery(datasetQuery, true) }
+                    onClick={() => setDatasetQuery(datasetQuery, true) }
                 >
                     Try it
                 </div>
@@ -65,11 +65,11 @@ const TagExample = ({ datasetQuery, setQuery }) =>
         </p>
     </div>
 
-const TagEditorHelp = ({ setQuery, sampleDatasetId }) => {
+const TagEditorHelp = ({ setDatasetQuery, sampleDatasetId }) => {
     let setQueryWithSampleDatasetId = null;
     if (sampleDatasetId != null) {
         setQueryWithSampleDatasetId = (dataset_query, run) => {
-            setQuery({
+            setDatasetQuery({
                 ...dataset_query,
                 database: sampleDatasetId
             }, run);
@@ -91,7 +91,7 @@ const TagEditorHelp = ({ setQuery, sampleDatasetId }) => {
                 question. When this filter widget is filled in, that value replaces the variable in the SQL
                 template.
             </p>
-            <TagExample datasetQuery={EXAMPLES.variable} setQuery={setQueryWithSampleDatasetId} />
+            <TagExample datasetQuery={EXAMPLES.variable} setDatasetQuery={setQueryWithSampleDatasetId} />
 
             <h4>Dimensions</h4>
             <p>
@@ -109,13 +109,13 @@ const TagEditorHelp = ({ setQuery, sampleDatasetId }) => {
                 template. If "variable" is set, then the entire clause is placed into the template.
                 If not, then the entire clause is ignored.
             </p>
-            <TagExample datasetQuery={EXAMPLES.optional} setQuery={setQueryWithSampleDatasetId} />
+            <TagExample datasetQuery={EXAMPLES.optional} setDatasetQuery={setQueryWithSampleDatasetId} />
 
             <p>
                 To use multiple optional clauses you can include at least one non-optional WHERE clause
                 followed by optional clauses starting with "AND".
             </p>
-            <TagExample datasetQuery={EXAMPLES.multipleOptional} setQuery={setQueryWithSampleDatasetId} />
+            <TagExample datasetQuery={EXAMPLES.multipleOptional} setDatasetQuery={setQueryWithSampleDatasetId} />
 
             <p>
                 <a href="http://www.metabase.com/docs/latest/users-guide/start" target="_blank" data-metabase-event="QueryBuilder;Template Tag Documentation Click">Read the full documentation</a>
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 80168e06a8757362777f505ebb4ff0b2d5959566..fc385c58253f903783ba7c8a12e63dfe919e7577 100644
--- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx
+++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx
@@ -5,6 +5,9 @@ import Toggle from "metabase/components/Toggle.jsx";
 import Select, { Option } from "metabase/components/Select.jsx";
 import ParameterValueWidget from "metabase/dashboard/components/parameters/ParameterValueWidget.jsx";
 
+import { parameterOptionsForField } from "metabase/meta/Dashboard";
+import Field from "metabase/meta/metadata/Field";
+
 import _ from "underscore";
 
 export default class TagEditorParam extends Component {
@@ -46,7 +49,28 @@ export default class TagEditorParam extends Component {
             this.props.onUpdate({
                 ...this.props.tag,
                 type: type,
-                dimension: undefined
+                dimension: undefined,
+                widget_type: undefined
+            });
+        }
+    }
+
+    setDimension(fieldId) {
+        const { tag, onUpdate, databaseFields } = this.props;
+        const dimension = ["field-id", fieldId];
+        if (!_.isEqual(tag.dimension !== dimension)) {
+            const field = _.findWhere(databaseFields, { id: fieldId });
+            const options = parameterOptionsForField(new Field(field));
+            let widget_type;
+            if (tag.widget_type && _.findWhere(options, { type: tag.widget_type })) {
+                widget_type = tag.widget_type;
+            } else if (options.length > 0) {
+                widget_type = options[0].type;
+            }
+            onUpdate({
+                ...tag,
+                dimension,
+                widget_type
             });
         }
     }
@@ -60,11 +84,19 @@ export default class TagEditorParam extends Component {
             dabaseHasSchemas = schemas.length > 1;
         }
 
+        let widgetOptions;
+        if (tag.type === "dimension" && tag.dimension) {
+            const field = _.findWhere(databaseFields, { id: tag.dimension[1] });
+            if (field) {
+                widgetOptions = parameterOptionsForField(new Field(field));
+            }
+        }
+
         return (
             <div className="pb2 mb2 border-bottom border-dark">
-                <h3 className="pb1">{tag.name}</h3>
+                <h3 className="pb2">{tag.name}</h3>
 
-                <div className="pb2">
+                <div className="pb1">
                     <h5 className="pb1 text-normal">Filter label</h5>
                     <input
                         type="text"
@@ -74,7 +106,7 @@ export default class TagEditorParam extends Component {
                     />
                 </div>
 
-                <div className="pb2">
+                <div className="pb1">
                     <h5 className="pb1 text-normal">Variable type</h5>
                     <Select
                         className="border-med bg-white block"
@@ -90,36 +122,13 @@ export default class TagEditorParam extends Component {
                     </Select>
                 </div>
 
-                { tag.type !== "dimension" &&
-                    <div className="flex align-center pb2">
-                        <h5 className="text-normal mr1">Required?</h5>
-                        <Toggle value={tag.required} onChange={(value) => this.setRequired(value)} />
-                    </div>
-                }
-
-                { tag.type !== "dimension" && tag.required &&
-                    <div className="pb2">
-                        <h5 className="pb1 text-normal">Default value</h5>
-                        <ParameterValueWidget
-                            parameter={{
-                                type: tag.type === "date" ? "date/single" : null
-                            }}
-                            value={tag.default}
-                            setValue={(value) => this.setParameterAttribute("default", value)}
-                            className="AdminSelect p1 text-bold text-grey-4 bordered border-med rounded bg-white"
-                            isEditing
-                            commitImmediately
-                        />
-                    </div>
-                }
-
                 { tag.type === "dimension" &&
-                    <div className="pb2">
+                    <div className="pb1">
                         <h5 className="pb1 text-normal">Field</h5>
                         <Select
                             className="border-med bg-white block"
                             value={Array.isArray(tag.dimension) ? tag.dimension[1] : null}
-                            onChange={(e) => this.setParameterAttribute("dimension", ["field-id", e.target.value])}
+                            onChange={(e) => this.setDimension(e.target.value)}
                             searchProp="name"
                             searchCaseInsensitive
                             isInitiallyOpen={!tag.dimension}
@@ -137,6 +146,48 @@ export default class TagEditorParam extends Component {
 
                     </div>
                 }
+
+                { widgetOptions && widgetOptions.length > 0 &&
+                    <div className="pb1">
+                        <h5 className="pb1 text-normal">Widget</h5>
+                        <Select
+                            className="border-med bg-white block"
+                            value={tag.widget_type}
+                            onChange={(e) => this.setParameterAttribute("widget_type", e.target.value)}
+                            isInitiallyOpen={!tag.widget_type}
+                            placeholder="Select…"
+                        >
+                            {[{ name: "None", type: undefined }].concat(widgetOptions).map(widgetOption =>
+                                <Option key={widgetOption.type} value={widgetOption.type}>
+                                    {widgetOption.name}
+                                </Option>
+                            )}
+                        </Select>
+                    </div>
+                }
+
+                { tag.type !== "dimension" &&
+                    <div className="flex align-center pb1">
+                        <h5 className="text-normal mr1">Required?</h5>
+                        <Toggle value={tag.required} onChange={(value) => this.setRequired(value)} />
+                    </div>
+                }
+
+                { ((tag.type !== "dimension" && tag.required) || (tag.type === "dimension" || tag.widget_type)) &&
+                    <div className="pb1">
+                        <h5 className="pb1 text-normal">Default value</h5>
+                        <ParameterValueWidget
+                            parameter={{
+                                type: tag.widget_type || (tag.type === "date" ? "date/single" : null)
+                            }}
+                            value={tag.default}
+                            setValue={(value) => this.setParameterAttribute("default", value)}
+                            className="AdminSelect p1 text-bold text-grey-4 bordered border-med rounded bg-white"
+                            isEditing
+                            commitImmediately
+                        />
+                    </div>
+                }
             </div>
         );
     }
diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx
index 17e7c9f1b46e114d859870d582015f64907a5191..b1dadd198f9b755d50034d3747934360d678a21f 100644
--- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx
+++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx
@@ -22,7 +22,7 @@ export default class TagEditorSidebar extends Component {
         onClose: PropTypes.func.isRequired,
         updateTemplateTag: PropTypes.func.isRequired,
         databaseFields: PropTypes.array,
-        setQuery: PropTypes.func.isRequired,
+        setDatasetQuery: PropTypes.func.isRequired,
         sampleDatasetId: PropTypes.number,
     };
 
@@ -63,7 +63,7 @@ export default class TagEditorSidebar extends Component {
                     { section === "settings" ?
                         <SettingsPane tags={tags} onUpdate={this.props.updateTemplateTag} databaseFields={this.props.databaseFields}/>
                     :
-                        <TagEditorHelp sampleDatasetId={this.props.sampleDatasetId} setQuery={this.props.setQuery}/>
+                        <TagEditorHelp sampleDatasetId={this.props.sampleDatasetId} setDatasetQuery={this.props.setDatasetQuery}/>
                     }
                 </div>
             </div>
diff --git a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
index dc315aea3fdb385b87b1428ed602e067a6d3d161..e0fddf9f11224e94198af92083e27dd7dae97aee 100644
--- a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
+++ b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
@@ -16,26 +16,30 @@ import QueryVisualization from "../components/QueryVisualization.jsx";
 import DataReference from "../components/dataref/DataReference.jsx";
 import TagEditorSidebar from "../components/template_tags/TagEditorSidebar.jsx";
 import SavedQuestionIntroModal from "../components/SavedQuestionIntroModal.jsx";
+import ActionsWidget from "../components/ActionsWidget.jsx";
 
 import {
-    card,
-    originalCard,
-    databases,
-    queryResult,
-    parameterValues,
-    isDirty,
-    isNew,
-    isObjectDetail,
-    tables,
-    tableMetadata,
-    tableForeignKeys,
-    tableForeignKeyReferences,
-    uiControls,
+    getCard,
+    getOriginalCard,
+    getLastRunCard,
+    getDatabases,
+    getQueryResult,
+    getParameterValues,
+    getIsDirty,
+    getIsNew,
+    getIsObjectDetail,
+    getTables,
+    getTableMetadata,
+    getTableForeignKeys,
+    getTableForeignKeyReferences,
+    getUiControls,
     getParametersWithValues,
     getDatabaseFields,
     getSampleDatasetId,
     getNativeDatabases,
     getIsRunnable,
+    getIsResultDirty,
+    getMode,
 } from "../selectors";
 
 import { getUserIsAdmin } from "metabase/selectors/user";
@@ -70,21 +74,28 @@ const mapStateToProps = (state, props) => {
         isAdmin:                   getUserIsAdmin(state, props),
         fromUrl:                   props.location.query.from,
 
-        card:                      card(state),
-        originalCard:              originalCard(state),
-        query:                     state.qb.card && state.qb.card.dataset_query,  // TODO: EOL, redundant
-        parameterValues:           parameterValues(state),
-        databases:                 databases(state),
+        mode:                      getMode(state),
+
+        card:                      getCard(state),
+        originalCard:              getOriginalCard(state),
+        lastRunCard:               getLastRunCard(state),
+
+        parameterValues:           getParameterValues(state),
+
+        databases:                 getDatabases(state),
         nativeDatabases:           getNativeDatabases(state),
-        tables:                    tables(state),
-        tableMetadata:             tableMetadata(state),
-        tableForeignKeys:          tableForeignKeys(state),
-        tableForeignKeyReferences: tableForeignKeyReferences(state),
-        result:                    queryResult(state),
-        isDirty:                   isDirty(state),
-        isNew:                     isNew(state),
-        isObjectDetail:            isObjectDetail(state),
-        uiControls:                uiControls(state),
+        tables:                    getTables(state),
+        tableMetadata:             getTableMetadata(state),
+        tableForeignKeys:          getTableForeignKeys(state),
+        tableForeignKeyReferences: getTableForeignKeyReferences(state),
+
+        result:                    getQueryResult(state),
+
+        isDirty:                   getIsDirty(state),
+        isNew:                     getIsNew(state),
+        isObjectDetail:            getIsObjectDetail(state),
+
+        uiControls:                getUiControls(state),
         parameters:                getParametersWithValues(state),
         databaseFields:            getDatabaseFields(state),
         sampleDatasetId:           getSampleDatasetId(state),
@@ -94,6 +105,7 @@ const mapStateToProps = (state, props) => {
         isEditing:                 state.qb.uiControls.isEditing,
         isRunning:                 state.qb.uiControls.isRunning,
         isRunnable:                getIsRunnable(state),
+        isResultDirty:             getIsResultDirty(state),
 
         loadTableAndForeignKeysFn: loadTableAndForeignKeys,
         autocompleteResultsFn:     (prefix) => autocompleteResults(state.qb.card, prefix),
@@ -118,6 +130,10 @@ export default class QueryBuilder extends Component {
 
         // TODO: React tells us that forceUpdate() is not the best thing to use, so ideally we can find a different way to trigger this
         this.forceUpdateDebounced = _.debounce(this.forceUpdate.bind(this), 400);
+
+        this.state = {
+            legacy: true
+        }
     }
 
     componentWillMount() {
@@ -138,9 +154,9 @@ export default class QueryBuilder extends Component {
 
         if (nextProps.location.action === "POP" && getURL(nextProps.location) !== getURL(this.props.location)) {
             this.props.popState(nextProps.location);
-        } else if (this.props.location.query.tutorial === undefined && nextProps.location.query.tutorial !== undefined) {
+        } else if (this.props.location.hash !== "#?tutorial" && nextProps.location.hash === "#?tutorial") {
             this.props.initializeQB(nextProps.location, nextProps.params);
-        } else if (getURL(nextProps.location) === "/q" && getURL(this.props.location) !== "/q") {
+        } else if (getURL(nextProps.location) === "/question" && getURL(this.props.location) !== "/question") {
             this.props.initializeQB(nextProps.location, nextProps.params);
         }
     }
@@ -170,7 +186,17 @@ export default class QueryBuilder extends Component {
     }
 
     render() {
-        const { card, isDirty, databases, uiControls } = this.props;
+        return (
+            <div className="flex-full flex relative">
+                <LegacyQueryBuilder {...this.props} />
+            </div>
+        )
+    }
+}
+
+class LegacyQueryBuilder extends Component {
+    render() {
+        const { card, isDirty, databases, uiControls, mode } = this.props;
 
         // if we don't have a card at all or no databases then we are initializing, so keep it simple
         if (!card || !databases) {
@@ -180,6 +206,8 @@ export default class QueryBuilder extends Component {
         }
 
         const showDrawer = uiControls.isShowingDataReference || uiControls.isShowingTemplateTagsEditor;
+        const ModeFooter = mode && mode.ModeFooter;
+
         return (
             <div className="flex-full relative">
                 <div className={cx("QueryBuilder flex flex-column bg-white spread", {"QueryBuilder--showSideDrawer": showDrawer})}>
@@ -189,17 +217,28 @@ export default class QueryBuilder extends Component {
 
                     <div id="react_qb_editor" className="z2">
                         { card && card.dataset_query && card.dataset_query.type === "native" ?
-                            <NativeQueryEditor {...this.props} isOpen={!card.dataset_query.native.query || isDirty} />
+                            <NativeQueryEditor
+                                {...this.props}
+                                isOpen={!card.dataset_query.native.query || isDirty}
+                                datasetQuery={card && card.dataset_query}
+                            />
                         :
                             <div className="wrapper">
-                                <GuiQueryEditor {...this.props}/>
+                                <GuiQueryEditor
+                                    {...this.props}
+                                    datasetQuery={card && card.dataset_query}
+                                />
                             </div>
                         }
                     </div>
 
                     <div ref="viz" id="react_qb_viz" className="flex z1" style={{ "transition": "opacity 0.25s ease-in-out" }}>
-                        <QueryVisualization {...this.props} />
+                        <QueryVisualization {...this.props} className="full wrapper mb2 z1" />
                     </div>
+
+                    { ModeFooter &&
+                        <ModeFooter {...this.props} className="flex-no-shrink" />
+                    }
                 </div>
 
                 <div className={cx("SideDrawer", { "SideDrawer--show": showDrawer })}>
@@ -219,6 +258,8 @@ export default class QueryBuilder extends Component {
                 { uiControls.isShowingNewbModal &&
                     <SavedQuestionIntroModal onClose={() => this.props.closeQbNewbModal()} />
                 }
+
+                <ActionsWidget {...this.props} className="z2 absolute bottom right" />
             </div>
         );
     }
diff --git a/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx b/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx
index d285d4c94d8dc3c1d6b1402e76bbc91ca5a3f36c..faf973c0dcde01a03a5344df4eacb9b05aad9e71 100644
--- a/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx
+++ b/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx
@@ -5,7 +5,7 @@ import { connect } from "react-redux";
 
 import EmbedWidget from "metabase/public/components/widgets/EmbedWidget";
 
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import { getParameters } from "metabase/meta/Card";
 import { createPublicLink, deletePublicLink, updateEnableEmbedding, updateEmbeddingParams, } from "../actions";
diff --git a/frontend/src/metabase/query_builder/reducers.js b/frontend/src/metabase/query_builder/reducers.js
index 67a159764475a415bdb1890993ffb749e60b5def..7e49ee13605e1f99eecc60fddfea33c6f9540ccb 100644
--- a/frontend/src/metabase/query_builder/reducers.js
+++ b/frontend/src/metabase/query_builder/reducers.js
@@ -1,6 +1,6 @@
 import Utils from "metabase/lib/utils";
 import { handleActions } from "redux-actions";
-import { assoc } from "icepick";
+import { assoc, dissoc } from "icepick";
 
 import {
     RESET_QB,
@@ -28,7 +28,7 @@ import {
     SET_QUERY_DATABASE,
     SET_QUERY_SOURCE_TABLE,
     SET_QUERY_MODE,
-    SET_QUERY,
+    SET_DATASET_QUERY,
     RUN_QUERY,
     CANCEL_QUERY,
     QUERY_COMPLETED,
@@ -49,7 +49,7 @@ export const uiControls = handleActions({
 
     [TOGGLE_DATA_REFERENCE]: { next: (state, { payload }) => ({ ...state, isShowingDataReference: !state.isShowingDataReference, isShowingTemplateTagsEditor: false }) },
     [TOGGLE_TEMPLATE_TAGS_EDITOR]: { next: (state, { payload }) => ({ ...state, isShowingTemplateTagsEditor: !state.isShowingTemplateTagsEditor, isShowingDataReference: false }) },
-    [SET_QUERY]: { next: (state, { payload }) => ({ ...state, isShowingTemplateTagsEditor: payload.openTemplateTagsEditor }) },
+    [SET_DATASET_QUERY]: { next: (state, { payload }) => ({ ...state, isShowingTemplateTagsEditor: payload.openTemplateTagsEditor }) },
     [CLOSE_QB_TUTORIAL]: { next: (state, { payload }) => ({ ...state, isShowingTutorial: false }) },
     [CLOSE_QB_NEWB_MODAL]: { next: (state, { payload }) => ({ ...state, isShowingNewbModal: false }) },
 
@@ -92,7 +92,7 @@ export const card = handleActions({
     [SET_QUERY_MODE]: { next: (state, { payload }) => payload },
     [SET_QUERY_DATABASE]: { next: (state, { payload }) => payload },
     [SET_QUERY_SOURCE_TABLE]: { next: (state, { payload }) => payload },
-    [SET_QUERY]: { next: (state, { payload }) => payload.card },
+    [SET_DATASET_QUERY]: { next: (state, { payload }) => payload.card },
 
     [QUERY_COMPLETED]: { next: (state, { payload }) => ({ ...state, display: payload.cardDisplay }) },
 
@@ -141,6 +141,11 @@ export const tableForeignKeyReferences = handleActions({
     [LOAD_OBJECT_DETAIL_FK_REFERENCES]: { next: (state, { payload }) => payload}
 }, null);
 
+export const lastRunCard = handleActions({
+    [RESET_QB]: { next: (state, { payload }) => null },
+    [QUERY_COMPLETED]: { next: (state, { payload }) => payload.card },
+    [QUERY_ERRORED]: { next: (state, { payload }) => null },
+}, null);
 
 // the result of a query execution.  optionally an error if the query fails to complete successfully.
 export const queryResult = handleActions({
@@ -158,7 +163,7 @@ export const queryExecutionPromise = handleActions({
 }, null);
 
 export const parameterValues = handleActions({
-    [SET_PARAMETER_VALUE]: { next: (state, { payload: { id, value }}) => assoc(state, id, value) }
+    [SET_PARAMETER_VALUE]: { next: (state, { payload: { id, value }}) => value == null ? dissoc(state, id) : assoc(state, id, value) }
 }, {});
 
 export const currentState = handleActions({
diff --git a/frontend/src/metabase/query_builder/selectors.js b/frontend/src/metabase/query_builder/selectors.js
index de951ce0c3c2bebac3a1aae70d09436582e208cc..9f3472fe8c25ac37b183a8b487b70fdb132c6afb 100644
--- a/frontend/src/metabase/query_builder/selectors.js
+++ b/frontend/src/metabase/query_builder/selectors.js
@@ -6,36 +6,40 @@ import { getTemplateTags } from "metabase/meta/Card";
 import { getTemplateTagParameters } from "metabase/meta/Parameter";
 
 import { isCardDirty, isCardRunnable } from "metabase/lib/card";
-import { parseFieldTarget } from "metabase/lib/query_time";
+import { parseFieldTargetId } from "metabase/lib/query_time";
 import { isPK } from "metabase/lib/types";
 import Query from "metabase/lib/query";
+import Utils from "metabase/lib/utils";
 
-export const uiControls                = state => state.qb.uiControls;
+export const getUiControls      = state => state.qb.uiControls;
 
-export const card                      = state => state.qb.card;
-export const originalCard              = state => state.qb.originalCard;
-export const parameterValues           = state => state.qb.parameterValues;
+export const getCard            = state => state.qb.card;
+export const getOriginalCard    = state => state.qb.originalCard;
+export const getLastRunCard     = state => state.qb.lastRunCard;
 
-export const isDirty = createSelector(
-    [card, originalCard],
+export const getParameterValues = state => state.qb.parameterValues;
+export const getQueryResult     = state => state.qb.queryResult;
+
+export const getIsDirty = createSelector(
+    [getCard, getOriginalCard],
     (card, originalCard) => {
         return isCardDirty(card, originalCard);
     }
 );
 
-export const isNew = (state) => state.qb.card && !state.qb.card.id;
+export const getIsNew = (state) => state.qb.card && !state.qb.card.id;
 
 export const getDatabaseId = createSelector(
-    [card],
+    [getCard],
     (card) => card && card.dataset_query && card.dataset_query.database
 );
 
-export const databases                 = state => state.qb.databases;
-export const tableForeignKeys          = state => state.qb.tableForeignKeys;
-export const tableForeignKeyReferences = state => state.qb.tableForeignKeyReferences;
+export const getDatabases                 = state => state.qb.databases;
+export const getTableForeignKeys          = state => state.qb.tableForeignKeys;
+export const getTableForeignKeyReferences = state => state.qb.tableForeignKeyReferences;
 
-export const tables = createSelector(
-    [getDatabaseId, databases],
+export const getTables = createSelector(
+    [getDatabaseId, getDatabases],
     (databaseId, databases) => {
         if (databaseId != null && databases && databases.length > 0) {
             let db = _.findWhere(databases, { id: databaseId });
@@ -49,14 +53,13 @@ export const tables = createSelector(
 );
 
 export const getNativeDatabases = createSelector(
-    databases,
+    [getDatabases],
     (databases) =>
         databases && databases.filter(db => db.native_permissions === "write")
 )
 
-export const tableMetadata = createSelector(
-    state => state.qb.tableMetadata,
-    databases,
+export const getTableMetadata = createSelector(
+    [state => state.qb.tableMetadata, getDatabases],
     (tableMetadata, databases) => tableMetadata && {
         ...tableMetadata,
         db: _.findWhere(databases, { id: tableMetadata.db_id })
@@ -64,7 +67,7 @@ export const tableMetadata = createSelector(
 )
 
 export const getSampleDatasetId = createSelector(
-    [databases],
+    [getDatabases],
     (databases) => {
         const sampleDataset = _.findWhere(databases, { is_sample: true });
         return sampleDataset && sampleDataset.id;
@@ -76,8 +79,8 @@ export const getDatabaseFields = createSelector(
     (databaseId, databaseFields) => databaseFields[databaseId]
 );
 
-export const isObjectDetail = createSelector(
-    [state => state.qb.queryResult],
+export const getIsObjectDetail = createSelector(
+    [getQueryResult],
     (queryResult) => {
         if (!queryResult || !queryResult.json_query) {
             return false;
@@ -114,7 +117,7 @@ export const isObjectDetail = createSelector(
                     if (Array.isArray(filter) &&
                             filter.length === 3 &&
                             filter[0] === "=" &&
-                               parseFieldTarget(filter[1]) === pkField &&
+                               parseFieldTargetId(filter[1]) === pkField &&
                             filter[2] !== null) {
                         // well, all of our conditions have passed so we have an object detail query here
                         response = true;
@@ -127,24 +130,36 @@ export const isObjectDetail = createSelector(
     }
 );
 
-export const queryResult = createSelector(
-    [state => state.qb.queryResult],
-    (queryResult) => queryResult
-);
+
+
+import { getMode as getMode_ } from "metabase/qb/lib/modes";
+
+export const getMode = createSelector(
+    [getLastRunCard, getTableMetadata],
+    (card, tableMetadata) => getMode_(card, tableMetadata)
+)
 
 export const getImplicitParameters = createSelector(
-    [card],
+    [getCard],
     (card) =>
         getTemplateTagParameters(getTemplateTags(card))
 );
 
+export const getModeParameters = createSelector(
+    [getLastRunCard, getTableMetadata, getMode],
+    (card, tableMetadata, mode) =>
+        (card && tableMetadata && mode && mode.getModeParameters) ?
+            mode.getModeParameters(card, tableMetadata) :
+            []
+);
+
 export const getParameters = createSelector(
-    [getImplicitParameters],
-    (implicitParameters) => implicitParameters
+    [getModeParameters, getImplicitParameters],
+    (modeParameters, implicitParameters) => [...modeParameters, ...implicitParameters]
 );
 
 export const getParametersWithValues = createSelector(
-    [getParameters, parameterValues],
+    [getParameters, getParameterValues],
     (parameters, values) =>
         parameters.map(parameter => ({
             ...parameter,
@@ -153,6 +168,20 @@ export const getParametersWithValues = createSelector(
 );
 
 export const getIsRunnable = createSelector(
-    [card, tableMetadata],
+    [getCard, getTableMetadata],
     (card, tableMetadata) => isCardRunnable(card, tableMetadata)
 )
+
+const getLastRunDatasetQuery = createSelector([getLastRunCard], (card) => card && card.dataset_query);
+const getNextRunDatasetQuery = createSelector([getCard], (card) => card && card.dataset_query);
+
+const getLastRunParameters = createSelector([getQueryResult], (queryResult) => queryResult && queryResult.json_query.parameters || [])
+const getLastRunParameterValues = createSelector([getLastRunParameters], (parameters) => parameters.map(parameter => parameter.value))
+const getNextRunParameterValues = createSelector([getParametersWithValues], (parameters) => parameters.map(parameter => parameter.value))
+
+export const getIsResultDirty = createSelector(
+    [getLastRunDatasetQuery, getNextRunDatasetQuery, getLastRunParameterValues, getNextRunParameterValues],
+    (lastDatasetQuery, nextDatasetQuery, lastParameters, nextParameters) => {
+        return !Utils.equals(lastDatasetQuery, nextDatasetQuery) || !Utils.equals(lastParameters, nextParameters);
+    }
+)
diff --git a/frontend/src/metabase/questions/collections.js b/frontend/src/metabase/questions/collections.js
index 98ffd30aa0967674657997ae18af6a66f19d9069..592417105a325b7ba5ad1890a68ff19bc568216d 100644
--- a/frontend/src/metabase/questions/collections.js
+++ b/frontend/src/metabase/questions/collections.js
@@ -2,7 +2,7 @@
 import { createAction, createThunkAction, handleActions, combineReducers } from "metabase/lib/redux";
 import { reset } from 'redux-form';
 import { replace } from "react-router-redux";
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import _ from "underscore";
 
diff --git a/frontend/src/metabase/questions/components/CollectionBadge.jsx b/frontend/src/metabase/questions/components/CollectionBadge.jsx
index 143520b64333f5aa63cb28db9bc50eef5d7dfa91..17cb724c593c6a9675ae80f29e048a7d60b49483 100644
--- a/frontend/src/metabase/questions/components/CollectionBadge.jsx
+++ b/frontend/src/metabase/questions/components/CollectionBadge.jsx
@@ -1,7 +1,7 @@
 import React, { Component, PropTypes } from "react";
 import { Link } from "react-router";
 
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import Color from "color";
 import cx from "classnames";
diff --git a/frontend/src/metabase/questions/components/Item.jsx b/frontend/src/metabase/questions/components/Item.jsx
index b8f5003d4fd7c20809de6f5504c96cc032442fd3..0f24593d33436ab5a8f50609763634927756e878 100644
--- a/frontend/src/metabase/questions/components/Item.jsx
+++ b/frontend/src/metabase/questions/components/Item.jsx
@@ -14,7 +14,7 @@ import MoveToCollection from "../containers/MoveToCollection.jsx";
 import Labels from "./Labels.jsx";
 import CollectionBadge from "./CollectionBadge.jsx";
 
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 const ITEM_ICON_SIZE = 20;
 
@@ -122,7 +122,7 @@ Item.propTypes = {
 const ItemBody = pure(({ entity, id, name, description, labels, favorite, collection, setFavorited, onEntityClick }) =>
     <div className={S.itemBody}>
         <div className={cx('flex', S.itemTitle)}>
-            <Link to={Urls.card(id)} className={cx(S.itemName)} onClick={onEntityClick && ((e) => { e.preventDefault(); onEntityClick(entity); })}>
+            <Link to={Urls.question(id)} className={cx(S.itemName)} onClick={onEntityClick && ((e) => { e.preventDefault(); onEntityClick(entity); })}>
                 {name}
             </Link>
             { collection &&
diff --git a/frontend/src/metabase/questions/components/Labels.jsx b/frontend/src/metabase/questions/components/Labels.jsx
index cadf7680243fc2acd3dc436d3c45eac04b524f1f..dabc0137ce8a198158efdd891d5a8ebef9123e32 100644
--- a/frontend/src/metabase/questions/components/Labels.jsx
+++ b/frontend/src/metabase/questions/components/Labels.jsx
@@ -3,7 +3,7 @@ import React, { Component, PropTypes } from "react";
 import { Link } from "react-router";
 import S from "./Labels.css";
 import color from 'color'
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import EmojiIcon from "metabase/components/EmojiIcon.jsx"
 
diff --git a/frontend/src/metabase/questions/questions.js b/frontend/src/metabase/questions/questions.js
index 9d1970c246abe09a32a35cda55ae91100af7ecfe..056815d94ee2c1828401aafc862f112d7af16502 100644
--- a/frontend/src/metabase/questions/questions.js
+++ b/frontend/src/metabase/questions/questions.js
@@ -7,7 +7,7 @@ import _ from "underscore";
 
 import { inflect } from "metabase/lib/formatting";
 import MetabaseAnalytics from "metabase/lib/analytics";
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import { push, replace } from "react-router-redux";
 import { setRequestState } from "metabase/redux/requests";
diff --git a/frontend/src/metabase/reference/components/GuideDetail.jsx b/frontend/src/metabase/reference/components/GuideDetail.jsx
index bbb27cc09b74ceff2c85ff22c45cd4925aa6add1..faf42ad83f95d13464616e6e2b550c21fc823ad3 100644
--- a/frontend/src/metabase/reference/components/GuideDetail.jsx
+++ b/frontend/src/metabase/reference/components/GuideDetail.jsx
@@ -2,7 +2,9 @@ import React, { Component, PropTypes } from "react";
 import { Link } from "react-router";
 import pure from "recompose/pure";
 import cx from "classnames";
+
 import Icon from "metabase/components/Icon"
+import * as Urls from "metabase/lib/urls";
 
 import {
     getQuestionUrl,
@@ -21,7 +23,7 @@ const GuideDetail = ({
     const title = entity.display_name || entity.name;
     const { caveats, points_of_interest } = entity;
     const typeToLink = {
-        dashboard: `/dash/${entity.id}`,
+        dashboard: Urls.dashboard(entity.id),
         metric: getQuestionUrl({
             dbId: tables[entity.table_id] && tables[entity.table_id].db_id,
             tableId: entity.table_id,
diff --git a/frontend/src/metabase/reference/components/GuideDetailEditor.jsx b/frontend/src/metabase/reference/components/GuideDetailEditor.jsx
index 69070f0ebbb2c46801983a2e15abcbe230603e26..cc3eb93c86938cd50f407ac933ca224d2971f84e 100644
--- a/frontend/src/metabase/reference/components/GuideDetailEditor.jsx
+++ b/frontend/src/metabase/reference/components/GuideDetailEditor.jsx
@@ -60,7 +60,7 @@ const GuideDetailEditor = ({
             </div>
             <div className="py2">
                 { entities ?
-                    <Select 
+                    <Select
                         value={entities[formField.id.value]}
                         options={Object.values(entities)}
                         disabledOptionIds={selectedIds}
@@ -83,7 +83,7 @@ const GuideDetailEditor = ({
                         className={cx(selectClasses, 'inline-block', 'rounded', 'text-bold')}
                         triggerIconSize={12}
                         includeTables={true}
-                        query={{
+                        datasetQuery={{
                             query: {
                                 source_table: formField.type.value === 'table' &&
                                     Number.parseInt(formField.id.value)
@@ -99,7 +99,7 @@ const GuideDetailEditor = ({
                         databases={
                             Object.values(databases)
                                 .map(database => ({
-                                    ...database, 
+                                    ...database,
                                     tables: database.tables.map(tableId => tables[tableId])
                                 }))
                         }
@@ -110,7 +110,7 @@ const GuideDetailEditor = ({
                             .map(idTypePair => idTypePair[0])
                         }
                         setSourceTableFn={(tableId) => {
-                            const table = tables[tableId]; 
+                            const table = tables[tableId];
                             formField.id.onChange(table.id);
                             formField.type.onChange('table');
                             formField.points_of_interest.onChange(table.points_of_interest || '');
@@ -122,7 +122,7 @@ const GuideDetailEditor = ({
                             .map(idTypePair => idTypePair[0])
                         }
                         setSourceSegmentFn={(segmentId) => {
-                            const segment = segments[segmentId]; 
+                            const segment = segments[segmentId];
                             formField.id.onChange(segment.id);
                             formField.type.onChange('segment');
                             formField.points_of_interest.onChange(segment.points_of_interest || '');
@@ -147,10 +147,10 @@ const GuideDetailEditor = ({
                 <EditLabel>
                     { type === 'dashboard' ?
                             `Why is this dashboard the most important?` :
-                            `What is useful or interesting about this ${type}?` 
+                            `What is useful or interesting about this ${type}?`
                     }
                 </EditLabel>
-                <textarea 
+                <textarea
                     className={S.guideDetailEditorTextarea}
                     placeholder="Write something helpful here"
                     {...formField.points_of_interest}
@@ -162,17 +162,17 @@ const GuideDetailEditor = ({
                 <EditLabel>
                     { type === 'dashboard' ?
                             `Is there anything users of this dashboard should be aware of?` :
-                            `Anything users should be aware of about this ${type}?` 
+                            `Anything users should be aware of about this ${type}?`
                     }
-                </EditLabel>           
-                <textarea 
-                    className={S.guideDetailEditorTextarea} 
+                </EditLabel>
+                <textarea
+                    className={S.guideDetailEditorTextarea}
                     placeholder="Write something helpful here"
                     {...formField.caveats}
                     disabled={disabled}
                 />
             </div>
-            { type === 'metric' && 
+            { type === 'metric' &&
                 <div className={cx('mb2', { 'disabled' : disabled })}>
                     <EditLabel key="metricFieldsLabel">
                         Which 2-3 fields do you usually group this metric by?
diff --git a/frontend/src/metabase/reference/containers/ReferenceApp.jsx b/frontend/src/metabase/reference/containers/ReferenceApp.jsx
index 1b7de3034393e1f091b38d4b6b0373981c8d4fe7..f89d6b700e4d931c4d466fabb74049a8a085986e 100644
--- a/frontend/src/metabase/reference/containers/ReferenceApp.jsx
+++ b/frontend/src/metabase/reference/containers/ReferenceApp.jsx
@@ -1,6 +1,5 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component, PropTypes } from 'react';
-import ReactDom from 'react-dom';
 import { connect } from 'react-redux';
 
 import Sidebar from 'metabase/components/Sidebar.jsx';
diff --git a/frontend/src/metabase/reference/containers/ReferenceEntity.jsx b/frontend/src/metabase/reference/containers/ReferenceEntity.jsx
index 908a1fe84e316d6c6251da5e1620d491fc263912..b5d43d664d6c7a0c585bf59b9e4fe373ba92883b 100644
--- a/frontend/src/metabase/reference/containers/ReferenceEntity.jsx
+++ b/frontend/src/metabase/reference/containers/ReferenceEntity.jsx
@@ -1,6 +1,5 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component, PropTypes } from "react";
-import ReactDOM from "react-dom";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
 import { push } from "react-router-redux";
@@ -48,7 +47,7 @@ const mapStateToProps = (state, props) => {
     const entity = getData(state, props) || {};
     const guide = getGuide(state, props);
     const fields = getFields(state, props);
-    
+
     const initialValues = {
         important_fields: guide && guide.metric_important_fields &&
             guide.metric_important_fields[entity.id] &&
@@ -308,7 +307,7 @@ export default class ReferenceEntity extends Component {
                                             .map(fieldId => metadataFields[fieldId])
                                             .reduce((map, field) => ({ ...map, [field.id]: field }), {})
                                         }
-                                        databaseId={table.db_id} 
+                                        databaseId={table.db_id}
                                         metric={entity}
                                         title={ guide && guide.metric_important_fields[entity.id] ?
                                             "Other fields you can group this metric by" :
diff --git a/frontend/src/metabase/reference/containers/ReferenceEntityList.jsx b/frontend/src/metabase/reference/containers/ReferenceEntityList.jsx
index cf6e8e8cb7fd99795b3a3f03ead295dff11d7764..9f821341533866af791d903eec787c720cec6106 100644
--- a/frontend/src/metabase/reference/containers/ReferenceEntityList.jsx
+++ b/frontend/src/metabase/reference/containers/ReferenceEntityList.jsx
@@ -1,11 +1,11 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component, PropTypes } from "react";
-import ReactDOM from "react-dom";
 import { connect } from "react-redux";
 import moment from "moment";
 
 import visualizations from "metabase/visualizations";
 import { isQueryable } from "metabase/lib/table";
+import * as Urls from "metabase/lib/urls";
 
 import S from "metabase/components/List.css";
 import R from "metabase/reference/Reference.css";
@@ -58,7 +58,7 @@ const createListItem = (entity, index, section) =>
             }
             url={section.type !== 'questions' ?
                 `${section.id}/${entity.id}` :
-                `/card/${entity.id}`
+                Urls.question(entity.id)
             }
             icon={section.type === 'questions' ?
                 visualizations.get(entity.display).iconName :
diff --git a/frontend/src/metabase/reference/containers/ReferenceFieldsList.jsx b/frontend/src/metabase/reference/containers/ReferenceFieldsList.jsx
index 1f665afdf358e709ae94e72134f7aea221f2cf1a..b680dba9f4a54aa3a360db4bd9ad12c881bd8e75 100644
--- a/frontend/src/metabase/reference/containers/ReferenceFieldsList.jsx
+++ b/frontend/src/metabase/reference/containers/ReferenceFieldsList.jsx
@@ -1,6 +1,5 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component, PropTypes } from "react";
-import ReactDOM from "react-dom";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
 
diff --git a/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx b/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx
index a5640fb8d7295fb1b3d616207ce54a3a38edbcef..b34c861b019d881c0797b701c203b252bc49b08f 100644
--- a/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx
+++ b/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx
@@ -4,10 +4,12 @@ import { Link } from "react-router";
 import { connect } from 'react-redux';
 import { push } from 'react-router-redux';
 import { reduxForm } from "redux-form";
+
 import { assoc } from "icepick";
 import cx from "classnames";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
+import * as Urls from "metabase/lib/urls";
 
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
 import CreateDashboardModal from 'metabase/components/CreateDashboardModal.jsx';
@@ -225,7 +227,7 @@ export default class ReferenceGettingStartedGuide extends Component {
                             createDashboardFn={async (newDashboard) => {
                                 try {
                                     const action = await createDashboard(newDashboard, true);
-                                    push(`/dash/${action.payload.id}`);
+                                    push(Urls.dashboard(action.payload.id));
                                 }
                                 catch(error) {
                                     console.error(error);
@@ -632,4 +634,4 @@ const AdminInstructions = ({ children }) => // eslint-disable-line react/prop-ty
     </div>
 
 const SectionHeader = ({ trim, children }) => // eslint-disable-line react/prop-types
-    <h2 className={cx('text-dark text-measure', {  "mb0" : trim }, { "mb4" : !trim })}>{children}</h2> 
+    <h2 className={cx('text-dark text-measure', {  "mb0" : trim }, { "mb4" : !trim })}>{children}</h2>
diff --git a/frontend/src/metabase/reference/utils.js b/frontend/src/metabase/reference/utils.js
index e22840da4f48bf99e05eb563c75c6951927bee43..0a85e12cc89dcc5ec8ef40b417179cc031cae011 100644
--- a/frontend/src/metabase/reference/utils.js
+++ b/frontend/src/metabase/reference/utils.js
@@ -2,8 +2,9 @@ import { assoc, assocIn, chain } from "icepick";
 import _ from "underscore";
 
 import { titleize, humanize } from "metabase/lib/formatting";
-import { startNewCard, serializeCardForUrl } from "metabase/lib/card";
+import { startNewCard } from "metabase/lib/card";
 import { isPK } from "metabase/lib/types";
+import * as Urls from "metabase/lib/urls";
 
 export const idsToObjectMap = (ids, objects) => ids
     .map(id => objects[id])
@@ -76,11 +77,10 @@ export const tryUpdateData = async (fields, props) => {
                 const importantFieldIds = fields.important_fields.map(field => field.id);
                 const existingImportantFieldIds = guide.metric_important_fields && guide.metric_important_fields[entity.id];
 
-                const areFieldIdsIdentitical = existingImportantFieldIds && 
+                const areFieldIdsIdentitical = existingImportantFieldIds &&
                     existingImportantFieldIds.length === importantFieldIds.length &&
                     existingImportantFieldIds.every(id => importantFieldIds.includes(id));
-                
-                console.log(areFieldIdsIdentitical);
+
                 if (!areFieldIdsIdentitical) {
                     await updateMetricImportantFields(entity.id, importantFieldIds);
                     tryFetchData(props);
@@ -156,8 +156,8 @@ export const tryUpdateGuide = async (formFields, props) => {
     startLoading();
     try {
         const updateNewEntities = ({
-            entities, 
-            formFields, 
+            entities,
+            formFields,
             updateEntity
         }) => formFields.map(formField => {
             if (!formField.id) {
@@ -175,7 +175,7 @@ export const tryUpdateGuide = async (formFields, props) => {
 
             const newEntity = entities[formField.id];
             const updatedNewEntity = {
-                ...newEntity, 
+                ...newEntity,
                 ...editedEntity
             };
 
@@ -185,9 +185,9 @@ export const tryUpdateGuide = async (formFields, props) => {
         });
 
         const updateOldEntities = ({
-            newEntityIds, 
-            oldEntityIds, 
-            entities, 
+            newEntityIds,
+            oldEntityIds,
+            entities,
             updateEntity
         }) => oldEntityIds
             .filter(oldEntityId => !newEntityIds.includes(oldEntityId))
@@ -201,14 +201,14 @@ export const tryUpdateGuide = async (formFields, props) => {
                 );
 
                 const updatingOldEntity = updateEntity(updatedOldEntity);
-                
+
                 return [updatingOldEntity];
             });
         //FIXME: necessary because revision_message is a mandatory field
         // even though we don't actually keep track of changes to caveats/points_of_interest yet
         const updateWithRevisionMessage = updateEntity => entity => updateEntity(assoc(
             entity,
-            'revision_message', 
+            'revision_message',
             'Updated in Getting Started guide.'
         ));
 
@@ -218,9 +218,9 @@ export const tryUpdateGuide = async (formFields, props) => {
                 updateEntity: updateDashboard
             })
             .concat(updateOldEntities({
-                newEntityIds: formFields.most_important_dashboard ? 
+                newEntityIds: formFields.most_important_dashboard ?
                     [formFields.most_important_dashboard.id] : [],
-                oldEntityIds: guide.most_important_dashboard ? 
+                oldEntityIds: guide.most_important_dashboard ?
                     [guide.most_important_dashboard] :
                     [],
                 entities: dashboards,
@@ -239,7 +239,7 @@ export const tryUpdateGuide = async (formFields, props) => {
                 entities: metrics,
                 updateEntity: updateWithRevisionMessage(updateMetric)
             }));
-        
+
         const updatingMetricImportantFields = formFields.important_metrics
             .map(metricFormField => {
                 if (!metricFormField.id || !metricFormField.important_fields) {
@@ -248,17 +248,17 @@ export const tryUpdateGuide = async (formFields, props) => {
                 const importantFieldIds = metricFormField.important_fields
                     .map(field => field.id);
                 const existingImportantFieldIds = guide.metric_important_fields[metricFormField.id];
-                
-                const areFieldIdsIdentitical = existingImportantFieldIds && 
+
+                const areFieldIdsIdentitical = existingImportantFieldIds &&
                     existingImportantFieldIds.length === importantFieldIds.length &&
                     existingImportantFieldIds.every(id => importantFieldIds.includes(id));
                 if (areFieldIdsIdentitical) {
                     return [];
                 }
-                
+
                 return [updateMetricImportantFields(metricFormField.id, importantFieldIds)];
             });
-        
+
         const segmentFields = formFields.important_segments_and_tables
             .filter(field => field.type === 'segment');
 
@@ -299,7 +299,7 @@ export const tryUpdateGuide = async (formFields, props) => {
             guide.contact.name !== formFields.contact.name ?
                 [updateSetting({key: 'getting-started-contact-name', value: formFields.contact.name })] :
                 [];
-        
+
         const updatingContactEmail = guide.contact && formFields.contact &&
             guide.contact.email !== formFields.contact.email ?
                 [updateSetting({key: 'getting-started-contact-email', value: formFields.contact.email })] :
@@ -318,7 +318,7 @@ export const tryUpdateGuide = async (formFields, props) => {
 
         if (updatingData.length > 0) {
             await Promise.all(updatingData);
-            
+
             clearRequestState({statePath: ['reference', 'guide']});
 
             await fetchGuide();
@@ -431,7 +431,7 @@ export const getQuestion = ({dbId, tableId, fieldId, metricId, segmentId, getCou
     return question;
 };
 
-export const getQuestionUrl = getQuestionArgs => `/q#${serializeCardForUrl(getQuestion(getQuestionArgs))}`;
+export const getQuestionUrl = getQuestionArgs => Urls.question(null, getQuestion(getQuestionArgs));
 
 export const isGuideEmpty = ({
     things_to_know,
@@ -446,7 +446,7 @@ export const isGuideEmpty = ({
     most_important_dashboard ? false :
     important_metrics && important_metrics.length !== 0 ? false :
     important_segments && important_segments.length !== 0 ? false :
-    important_tables && important_tables.length !== 0 ? false : 
+    important_tables && important_tables.length !== 0 ? false :
     true;
 
 export const typeToLinkClass = {
diff --git a/frontend/src/metabase/routes-internal.js b/frontend/src/metabase/routes-internal.js
index 5ccc1f5e230f854508f86bc46fdc5dc83a047861..7250a151c1f39e4fd1fe8549c935ceb1a84563f7 100644
--- a/frontend/src/metabase/routes-internal.js
+++ b/frontend/src/metabase/routes-internal.js
@@ -3,6 +3,8 @@ import { Route } from "react-router";
 
 import Icon from "metabase/components/Icon.jsx";
 
+import cx from "classnames";
+
 const SIZES = [12, 16];
 
 const ListApp = () =>
@@ -94,10 +96,26 @@ class EmbedTestApp extends Component {
     }
 }
 
+// eslint-disable-next-line import/no-commonjs
+let colorStyles = require("!style!css?modules!postcss!metabase/css/core/colors.css");
+
+const ColorsApp = () =>
+    <div className="p2">
+        {Object.entries(colorStyles).map(([name, className]) =>
+            <div
+                className={cx(className, "rounded px1")}
+                style={{ paddingTop: "0.25em", paddingBottom: "0.25em", marginBottom: "0.25em" }}
+            >
+                {name}
+            </div>
+        )}
+    </div>
+
 export default (
     <Route>
         <Route path="list" component={ListApp} />
         <Route path="icons" component={IconsApp} />
+        <Route path="colors" component={ColorsApp} />
         <Route path="embed/:type/:uuid" component={EmbedTestApp} />
     </Route>
 );
diff --git a/frontend/src/metabase/routes.jsx b/frontend/src/metabase/routes.jsx
index 26285e188981bda6c8cadddadaa8053905382c08..1be0d20d54d4ef1eafad19b5edd4864de11cb470 100644
--- a/frontend/src/metabase/routes.jsx
+++ b/frontend/src/metabase/routes.jsx
@@ -151,11 +151,11 @@ export const getRoutes = (store) =>
                 <Route path="/" component={HomepageApp} />
 
                 {/* DASHBOARD */}
-                <Route path="/dash/:dashboardId" component={DashboardApp} />
+                <Route path="/dashboard/:dashboardId" component={DashboardApp} />
 
                 {/* QUERY BUILDER */}
-                <Route path="/card/:cardId" component={QueryBuilder} />
-                <Route path="/q" component={QueryBuilder} />
+                <Route path="/question" component={QueryBuilder} />
+                <Route path="/question/:cardId" component={QueryBuilder} />
 
                 {/* QUESTIONS */}
                 <Route path="/questions">
@@ -262,13 +262,13 @@ export const getRoutes = (store) =>
                 <IndexRedirect to="/_internal/list" />
             </Route>
 
+            {/* DEPRECATED */}
+            <Redirect from="/q" to="/question" />
+            <Redirect from="/card/:cardId" to="/question/:cardId" />
+            <Redirect from="/dash/:dashboardId" to="/dashboard/:dashboardId" />
+
             {/* MISC */}
             <Route path="/unauthorized" component={Unauthorized} />
             <Route path="/*" component={NotFound} />
-
-            {/* LEGACY */}
-            <Redirect from="/card" to="/questions" />
-            <Redirect from="/card/:cardId/:serializedCard" to="/questions/:cardId#:serializedCard" />
-            <Redirect from="/q/:serializedCard" to="/q#:serializedCard" />
         </Route>
     </Route>
diff --git a/frontend/src/metabase/selectors/metadata.js b/frontend/src/metabase/selectors/metadata.js
new file mode 100644
index 0000000000000000000000000000000000000000..744a84b3c609fc1c7d02a192457de3eb15be5315
--- /dev/null
+++ b/frontend/src/metabase/selectors/metadata.js
@@ -0,0 +1,5 @@
+
+export const getTables = (state) => state.metadata.tables;
+export const getFields = (state) => state.metadata.fields;
+export const getMetrics = (state) => state.metadata.metrics;
+export const getDatabases = (state) => Object.values(state.metadata.databases);
diff --git a/frontend/src/metabase/visualizations/components/ChartClickActions.jsx b/frontend/src/metabase/visualizations/components/ChartClickActions.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..0e3563eb51714831c71312168f6b487cb245f326
--- /dev/null
+++ b/frontend/src/metabase/visualizations/components/ChartClickActions.jsx
@@ -0,0 +1,91 @@
+/* @flow */
+
+import React, { Component, PropTypes } from "react";
+
+import Button from "metabase/components/Button";
+import Popover from "metabase/components/Popover";
+
+import type { ClickObject, ClickAction } from "metabase/meta/types/Visualization";
+import type { Card } from "metabase/meta/types/Card";
+
+type Props = {
+    clicked: ClickObject,
+    clickActions: ?ClickAction[],
+    onChangeCardAndRun: (card: ?Card) => void,
+    onClose: () => void
+};
+
+type State = {
+    popoverIndex: ?number;
+}
+
+export default class ChartClickActions extends Component<*, Props, State> {
+    state: State = {
+        popoverIndex: null
+    };
+
+    close = () => {
+        this.setState({ popoverIndex: null });
+        if (this.props.onClose) {
+            this.props.onClose();
+        }
+    }
+
+    render() {
+        const { clicked, clickActions, onChangeCardAndRun } = this.props;
+
+        if (!clicked || !clickActions || clickActions.length === 0) {
+            return null;
+        }
+
+        let { popoverIndex } = this.state;
+        if (clickActions.length === 1 && clickActions[0].popover && clickActions[0].default) {
+            popoverIndex = 0;
+        }
+
+        let popover;
+        if (popoverIndex != null && clickActions[popoverIndex].popover) {
+            const PopoverContent = clickActions[popoverIndex].popover;
+            popover = (
+                <PopoverContent
+                    onChangeCardAndRun={onChangeCardAndRun}
+                    onClose={this.close}
+                />
+            );
+        }
+
+        return (
+            <Popover
+                target={clicked.element}
+                targetEvent={clicked.event}
+                onClose={this.close}
+                verticalAttachments={["bottom", "top"]}
+                sizeToFit
+            >
+                { popover ?
+                    popover
+                :
+                    <div className="px1 pt1 flex flex-column">
+                        { clickActions.map((action, index) =>
+                            <Button
+                                key={index}
+                                className="mb1"
+                                medium
+                                onClick={() => {
+                                    if (action.popover) {
+                                        this.setState({ popoverIndex: index });
+                                    } else if (action.card) {
+                                        onChangeCardAndRun(action.card());
+                                        this.close();
+                                    }
+                                }}
+                            >
+                                {action.title}
+                            </Button>
+                        )}
+                    </div>
+                }
+            </Popover>
+        );
+    }
+}
diff --git a/frontend/src/metabase/visualizations/components/ChartTooltip.jsx b/frontend/src/metabase/visualizations/components/ChartTooltip.jsx
index 139d7b1af60213105164d7500b4412a4ba07f8fb..501b403030d05f48af9fe7fc1b59bb937f742384 100644
--- a/frontend/src/metabase/visualizations/components/ChartTooltip.jsx
+++ b/frontend/src/metabase/visualizations/components/ChartTooltip.jsx
@@ -19,7 +19,7 @@ export default class ChartTooltip extends Component {
     };
 
     componentWillReceiveProps({ hovered }) {
-        if (hovered && !Array.isArray(hovered.data)) {
+        if (hovered && hovered.data && !Array.isArray(hovered.data)) {
             console.warn("hovered.data should be an array of { key, value, col }", hovered.data);
         }
     }
diff --git a/frontend/src/metabase/visualizations/components/ChartWithLegend.jsx b/frontend/src/metabase/visualizations/components/ChartWithLegend.jsx
index fa659407d2129637a91c12d8da40c0ceb7305f0e..9218edfcb3009c2e5de766cbaa5f26a76b66ecfb 100644
--- a/frontend/src/metabase/visualizations/components/ChartWithLegend.jsx
+++ b/frontend/src/metabase/visualizations/components/ChartWithLegend.jsx
@@ -1,5 +1,4 @@
 import React, { Component, PropTypes } from "react";
-import ReactDOM from "react-dom";
 import styles from "./ChartWithLegend.css";
 
 import LegendVertical from "./LegendVertical.jsx";
diff --git a/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx b/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx
index 58e940631cb0ff861eebb44ca2c0d732fedaab54..d390fa5d61203b5a6877e1c46ec9e2596bac3563 100644
--- a/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx
+++ b/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx
@@ -1,5 +1,4 @@
 import React, { Component, PropTypes } from "react";
-import ReactDOM from "react-dom";
 
 import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
 
@@ -10,7 +9,6 @@ import MetabaseSettings from "metabase/lib/settings";
 import { formatNumber } from "metabase/lib/formatting";
 
 import ChartWithLegend from "./ChartWithLegend.jsx";
-import ChartTooltip from "./ChartTooltip.jsx";
 import LegacyChoropleth from "./LegacyChoropleth.jsx";
 import LeafletChoropleth from "./LeafletChoropleth.jsx";
 
@@ -120,7 +118,7 @@ export default class ChoroplethMap extends Component {
             );
         }
 
-        const { series, className, gridSize, hovered, onHoverChange, settings } = this.props;
+        const { series, className, gridSize, hovered, onHoverChange, onVisualizationClick, settings } = this.props;
         let { geoJson, minimalBounds } = this.state;
 
         // special case builtin maps to use legacy choropleth map
@@ -154,13 +152,30 @@ export default class ChoroplethMap extends Component {
         const getFeatureKey   = (feature) => String(feature.properties[keyProperty]).toLowerCase();
         const getFeatureValue = (feature) => valuesMap[getFeatureKey(feature)];
 
+        const heatMapColors = HEAT_MAP_COLORS.slice(0, Math.min(HEAT_MAP_COLORS.length, rows.length))
+
         const onHoverFeature = (hover) => {
             onHoverChange && onHoverChange(hover && {
-                index: HEAT_MAP_COLORS.indexOf(getColor(hover.feature)),
+                index: heatMapColors.indexOf(getColor(hover.feature)),
                 event: hover.event,
                 data: { key: getFeatureName(hover.feature), value: getFeatureValue(hover.feature)
             } })
         }
+        const onClickFeature = (click) => {
+            const featureKey = getFeatureKey(click.feature);
+            const row = _.find(rows, row => getRowKey(row) === featureKey);
+            if (onVisualizationClick && row !== undefined) {
+                onVisualizationClick({
+                    value: row[metricIndex],
+                    column: cols[metricIndex],
+                    dimensions: [{
+                        value: row[dimensionIndex],
+                        column: cols[dimensionIndex]
+                    }],
+                    event: click.event
+                });
+            }
+        }
 
         const valuesMap = {};
         const domain = []
@@ -169,15 +184,15 @@ export default class ChoroplethMap extends Component {
             domain.push(getRowValue(row));
         }
 
-        const groups = ss.ckmeans(domain, HEAT_MAP_COLORS.length);
+        const groups = ss.ckmeans(domain, heatMapColors.length);
 
-        var colorScale = d3.scale.quantile().domain(groups.map((cluster) => cluster[0])).range(HEAT_MAP_COLORS);
+        var colorScale = d3.scale.quantile().domain(groups.map((cluster) => cluster[0])).range(heatMapColors);
 
-        let legendColors = HEAT_MAP_COLORS.slice();
-        let legendTitles = HEAT_MAP_COLORS.map((color, index) => {
+        let legendColors = heatMapColors.slice();
+        let legendTitles = heatMapColors.map((color, index) => {
             const min = groups[index][0];
             const max = groups[index].slice(-1)[0];
-            return index === HEAT_MAP_COLORS.length - 1 ?
+            return index === heatMapColors.length - 1 ?
                 formatNumber(min) + " +" :
                 formatNumber(min) + " - " + formatNumber(max)
         });
@@ -213,6 +228,7 @@ export default class ChoroplethMap extends Component {
                         geoJson={geoJson}
                         getColor={getColor}
                         onHoverFeature={onHoverFeature}
+                        onClickFeature={onClickFeature}
                         projection={projection}
                     />
                 :
@@ -221,10 +237,10 @@ export default class ChoroplethMap extends Component {
                         geoJson={geoJson}
                         getColor={getColor}
                         onHoverFeature={onHoverFeature}
+                        onClickFeature={onClickFeature}
                         minimalBounds={minimalBounds}
                     />
                 }
-                <ChartTooltip series={series} hovered={hovered} />
             </ChartWithLegend>
         );
     }
diff --git a/frontend/src/metabase/visualizations/components/FunnelBar.jsx b/frontend/src/metabase/visualizations/components/FunnelBar.jsx
index 06f85d90983b55e9c39b9bd346324dfc67075756..79454994387531948a582ba368aa0fb591bb7687 100644
--- a/frontend/src/metabase/visualizations/components/FunnelBar.jsx
+++ b/frontend/src/metabase/visualizations/components/FunnelBar.jsx
@@ -1,14 +1,13 @@
 /* @flow */
 
 import React, { Component, PropTypes } from "react";
-import ReactDOM from "react-dom";
 
 import BarChart from "metabase/visualizations/visualizations/BarChart.jsx";
 
 import { getSettings } from "metabase/visualizations/lib/settings";
 import { assocIn } from "icepick";
 
-import type { VisualizationProps } from "metabase/visualizations";
+import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 export default class BarFunnel extends Component<*, VisualizationProps, *> {
     render() {
diff --git a/frontend/src/metabase/visualizations/components/FunnelNormal.jsx b/frontend/src/metabase/visualizations/components/FunnelNormal.jsx
index 6ce16aa79be19cd5458d8a67c09292a3967c9493..2f793f8d1949ac20073e631854455fb7cb360d85 100644
--- a/frontend/src/metabase/visualizations/components/FunnelNormal.jsx
+++ b/frontend/src/metabase/visualizations/components/FunnelNormal.jsx
@@ -1,12 +1,10 @@
 /* @flow */
 
 import React, { Component, PropTypes } from "react";
-import ReactDOM from "react-dom";
 
 import cx from "classnames";
 import styles from "./FunnelNormal.css";
 
-import ChartTooltip from "metabase/visualizations/components/ChartTooltip.jsx";
 import Ellipsified from "metabase/components/Ellipsified.jsx";
 import { formatValue } from "metabase/lib/formatting";
 import { getFriendlyName } from "metabase/visualizations/lib/utils";
@@ -15,7 +13,7 @@ import { normal } from "metabase/lib/colors";
 
 const DEFAULT_COLORS = Object.values(normal);
 
-import type { VisualizationProps, HoverData, HoverObject } from "metabase/visualizations";
+import type { VisualizationProps, HoverObject, ClickObject } from "metabase/meta/types/Visualization";
 
 type StepInfo = {
     value: number,
@@ -25,17 +23,18 @@ type StepInfo = {
         endBottom: number,
         endTop: number
     },
-    tooltip?: HoverData
+    hovered?: HoverObject,
+    clicked?: ClickObject,
 };
 
 export default class Funnel extends Component<*, VisualizationProps, *> {
     render() {
-        const { className, series, gridSize, hovered, onHoverChange } = this.props;
+        const { className, series, gridSize, hovered, onHoverChange, onVisualizationClick, visualizationIsClickable } = this.props;
 
         const dimensionIndex = 0;
         const metricIndex = 1;
         const cols = series[0].data.cols;
-        // $FlowFixMe: doesn't like intersection type
+        // $FlowFixMe
         const rows = series.map(s => s.data.rows[0]);
 
         const funnelSmallSize = gridSize && (gridSize.width < 7 || gridSize.height <= 5);
@@ -62,26 +61,40 @@ export default class Funnel extends Component<*, VisualizationProps, *> {
 
             infos[rowIndex + 1] = {
                 value: row[metricIndex],
+
                 graph: {
                     startBottom: infos[rowIndex].graph.endBottom,
                     startTop: infos[rowIndex].graph.endTop,
                     endTop: 0.5 + ((remaining / infos[0].value) / 2),
                     endBottom: 0.5 - ((remaining / infos[0].value) / 2),
                 },
-                tooltip: [
-                    {
-                        key: 'Step',
-                        value: formatDimension(row[dimensionIndex]),
-                    },
-                    {
-                        key: getFriendlyName(cols[metricIndex]),
-                        value: formatMetric(row[metricIndex]),
-                    },
-                    {
-                        key: 'Retained',
-                        value: formatPercent(row[metricIndex] / infos[0].value),
-                    },
-                ],
+
+                hovered: {
+                    index: rowIndex,
+                    data: [
+                        {
+                            key: 'Step',
+                            value: formatDimension(row[dimensionIndex]),
+                        },
+                        {
+                            key: getFriendlyName(cols[metricIndex]),
+                            value: formatMetric(row[metricIndex]),
+                        },
+                        {
+                            key: 'Retained',
+                            value: formatPercent(row[metricIndex] / infos[0].value),
+                        },
+                    ]
+                },
+
+                clicked: {
+                    value: row[metricIndex],
+                    column: cols[metricIndex],
+                    dimensions: [{
+                        value: row[dimensionIndex],
+                        column: cols[dimensionIndex],
+                    }]
+                }
             };
         });
 
@@ -90,6 +103,8 @@ export default class Funnel extends Component<*, VisualizationProps, *> {
 
         let initial = infos[0];
 
+        const isClickable = visualizationIsClickable(infos[0].clicked);
+
         return (
             <div className={cx(className, styles.Funnel, 'flex', {
                 [styles.Small]: funnelSmallSize,
@@ -112,11 +127,13 @@ export default class Funnel extends Component<*, VisualizationProps, *> {
                     <div key={index} className={cx(styles.FunnelStep, 'flex flex-column')}>
                         <Ellipsified className={styles.Head}>{formatDimension(rows[index + 1][dimensionIndex])}</Ellipsified>
                         <GraphSection
+                            className={cx({ "cursor-pointer": isClickable })}
                             index={index}
                             info={info}
                             infos={infos}
                             hovered={hovered}
                             onHoverChange={onHoverChange}
+                            onVisualizationClick={isClickable ? onVisualizationClick : null}
                         />
                         <div className={styles.Infos}>
                             <div className={styles.Title}>{formatPercent(info.value / initial.value)}</div>
@@ -124,8 +141,6 @@ export default class Funnel extends Component<*, VisualizationProps, *> {
                         </div>
                     </div>
                 )}
-                {/* Display tooltips following mouse */}
-                <ChartTooltip series={series} hovered={hovered} />
             </div>
         );
     }
@@ -136,24 +151,39 @@ const GraphSection = (
         info,
         infos,
         hovered,
-        onHoverChange
+        onHoverChange,
+        onVisualizationClick,
+        className,
     }: {
+        className?: string,
         index: number,
         info: StepInfo,
         infos: StepInfo[],
         hovered: ?HoverObject,
+        onVisualizationClick: ?((clicked: ?ClickObject) => void),
         onHoverChange: (hovered: ?HoverObject) => void
     }
 ) => {
     return (
         <svg
-            className={styles.Graph}
-            onMouseMove={event => onHoverChange({
-                index: index,
-                event: event.nativeEvent,
-                data: info.tooltip
-            })}
-            onMouseLeave={() => onHoverChange(null)}
+            className={cx(className, styles.Graph)}
+            onMouseMove={e => {
+                if (onHoverChange && info.hovered) {
+                    onHoverChange({
+                        ...info.hovered,
+                        event: e.nativeEvent
+                    })
+                }
+            }}
+            onMouseLeave={() => onHoverChange && onHoverChange(null)}
+            onClick={e => {
+                if (onVisualizationClick && info.clicked) {
+                    onVisualizationClick({
+                        ...info.clicked,
+                        event: e.nativeEvent
+                    })
+                }
+            }}
             viewBox="0 0 1 1"
             preserveAspectRatio="none"
         >
diff --git a/frontend/src/metabase/visualizations/components/LeafletChoropleth.jsx b/frontend/src/metabase/visualizations/components/LeafletChoropleth.jsx
index 6311f2d52e629754f96834c4643ef36842c788ed..b958b5b4cd7890e6cb7bbd17cea5b557622fecf9 100644
--- a/frontend/src/metabase/visualizations/components/LeafletChoropleth.jsx
+++ b/frontend/src/metabase/visualizations/components/LeafletChoropleth.jsx
@@ -10,7 +10,14 @@ import L from "leaflet";
 
 import { computeMinimalBounds } from "metabase/visualizations/lib/mapping";
 
-const LeafletChoropleth = ({ series, geoJson, minimalBounds = computeMinimalBounds(geoJson.features), getColor = () => normal.blue, onHoverFeature = () => {}, }) =>
+const LeafletChoropleth = ({
+    series,
+    geoJson,
+    minimalBounds = computeMinimalBounds(geoJson.features),
+    getColor = () => normal.blue,
+    onHoverFeature = () => {},
+    onClickFeature = () => {},
+}) =>
     <CardRenderer
         series={series}
         className="spread"
@@ -56,7 +63,13 @@ const LeafletChoropleth = ({ series, geoJson, minimalBounds = computeMinimalBoun
                     },
                     mouseout: (e) => {
                         onHoverFeature(null)
-                    }
+                    },
+                    click: (e) => {
+                        onClickFeature({
+                            feature: feature,
+                            event: e.originalEvent
+                        })
+                    },
                 });
             }
 
diff --git a/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx b/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx
index 3566f85b23bbecb59543b1182fc42ccf6cf610d0..da7b688e340f2a4792ad9d6143b5d620750030f3 100644
--- a/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx
+++ b/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx
@@ -1,5 +1,4 @@
 import React, { Component, PropTypes } from "react";
-import ReactDOM from "react-dom";
 
 import LeafletMap from "./LeafletMap.jsx";
 import L from "leaflet";
diff --git a/frontend/src/metabase/visualizations/components/LegacyChoropleth.jsx b/frontend/src/metabase/visualizations/components/LegacyChoropleth.jsx
index 89bfee6dc9695e010cb052856203c867037e4454..8059a0b659cdf9f63ca053f339304d5777e0177d 100644
--- a/frontend/src/metabase/visualizations/components/LegacyChoropleth.jsx
+++ b/frontend/src/metabase/visualizations/components/LegacyChoropleth.jsx
@@ -3,7 +3,7 @@ import React, { Component, PropTypes } from "react";
 import { isSameSeries } from "metabase/visualizations/lib/utils";
 import d3 from "d3";
 
-const LegacyChoropleth = ({ series, geoJson, projection, getColor, onHoverFeature }) => {
+const LegacyChoropleth = ({ series, geoJson, projection, getColor, onHoverFeature, onClickFeature }) => {
     let geo = d3.geo.path()
         .projection(projection);
 
@@ -27,6 +27,10 @@ const LegacyChoropleth = ({ series, geoJson, projection, getColor, onHoverFeatur
                                 event: e.nativeEvent
                             })}
                             onMouseLeave={() => onHoverFeature(null)}
+                            onClick={(e) => onClickFeature({
+                                feature: feature,
+                                event: e.nativeEvent
+                            })}
                         />
                     )}
                     </svg>
diff --git a/frontend/src/metabase/visualizations/components/LegendHeader.jsx b/frontend/src/metabase/visualizations/components/LegendHeader.jsx
index debcccb7630d521606375d3fb48f36c9262a2ed6..3574ac8732835fdb54eeb4788714f09b12e5fef5 100644
--- a/frontend/src/metabase/visualizations/components/LegendHeader.jsx
+++ b/frontend/src/metabase/visualizations/components/LegendHeader.jsx
@@ -5,7 +5,7 @@ import styles from "./Legend.css";
 import Icon from "metabase/components/Icon.jsx";
 import LegendItem from "./LegendItem.jsx";
 
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 
 import cx from "classnames";
 
@@ -33,7 +33,8 @@ export default class LegendHeader extends Component {
 
     static defaultProps = {
         series: [],
-        settings: {}
+        settings: {},
+        visualizationIsClickable: () => false
     };
 
     componentDidMount() {
@@ -48,12 +49,15 @@ export default class LegendHeader extends Component {
     }
 
     render() {
-        const { series, hovered, onRemoveSeries, actionButtons, onHoverChange, linkToCard, settings, description } = this.props;
+        const { series, hovered, onRemoveSeries, actionButtons, onHoverChange, linkToCard, settings, description, onVisualizationClick, visualizationIsClickable } = this.props;
         const showDots = series.length > 1;
         const isNarrow = this.state.width < 150;
         const showTitles = !showDots || !isNarrow;
 
         let colors = settings["graph.colors"] || DEFAULT_COLORS;
+
+        const isClickable = series.length > 0 && series[0].clicked && visualizationIsClickable(series[0].clicked);
+
         return (
             <div  className={cx(styles.LegendHeader, "Card-title mx1 flex flex-no-shrink flex-row align-center")}>
                 { series.map((s, index) => [
@@ -61,14 +65,17 @@ export default class LegendHeader extends Component {
                         key={index}
                         title={s.card.name}
                         description={description}
-                        href={linkToCard && s.card.id && Urls.card(s.card.id)}
+                        href={linkToCard && s.card.id && Urls.question(s.card.id)}
                         color={colors[index % colors.length]}
                         showDot={showDots}
                         showTitle={showTitles}
                         isMuted={hovered && hovered.index != null && index !== hovered.index}
                         onMouseEnter={() => onHoverChange && onHoverChange({ index })}
                         onMouseLeave={() => onHoverChange && onHoverChange(null) }
-                        />,
+                        onClick={isClickable && ((e) =>
+                            onVisualizationClick({ ...s.clicked, element: e.currentTarget })
+                        )}
+                    />,
                     onRemoveSeries && index > 0 &&
                       <Icon
                           name="close"
diff --git a/frontend/src/metabase/visualizations/components/LegendItem.jsx b/frontend/src/metabase/visualizations/components/LegendItem.jsx
index d62be8776af3340b953571d18170a183e9ea82e1..9a594e4317aee5a9370cfdea9afccbbe16dae67d 100644
--- a/frontend/src/metabase/visualizations/components/LegendItem.jsx
+++ b/frontend/src/metabase/visualizations/components/LegendItem.jsx
@@ -1,5 +1,4 @@
 import React, { Component, PropTypes } from "react";
-import ReactDOM from "react-dom";
 
 import Icon from "metabase/components/Icon.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
@@ -27,14 +26,19 @@ export default class LegendItem extends Component {
     };
 
     render() {
-        const { title, href, color, showDot, showTitle, isMuted, showTooltip, showDotTooltip, onMouseEnter, onMouseLeave, className, description } = this.props;
+        const { title, href, color, showDot, showTitle, isMuted, showTooltip, showDotTooltip, onMouseEnter, onMouseLeave, className, description, onClick } = this.props;
         return (
             <LegendLink
                 href={href}
-                className={cx(className, "LegendItem", "no-decoration flex align-center fullscreen-normal-text fullscreen-night-text", { mr1: showTitle, muted: isMuted })}
+                className={cx(className, "LegendItem", "no-decoration flex align-center fullscreen-normal-text fullscreen-night-text", {
+                    mr1: showTitle,
+                    muted: isMuted,
+                    "cursor-pointer": onClick
+                })}
                 style={{ overflowX: "hidden", flex: "0 1 auto" }}
                 onMouseEnter={onMouseEnter}
                 onMouseLeave={onMouseLeave}
+                onClick={onClick}
             >
                 { showDot &&
                     <Tooltip tooltip={title} isEnabled={showTooltip && showDotTooltip}>
diff --git a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx
index e6e4a4e6970c76a2e008b10fec8251e95ea056a0..e2716e6f90dd953c56e7bee892c527d16aa3366b 100644
--- a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx
+++ b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx
@@ -4,7 +4,6 @@ import React, { Component, PropTypes } from "react";
 
 import CardRenderer from "./CardRenderer.jsx";
 import LegendHeader from "./LegendHeader.jsx";
-import ChartTooltip from "./ChartTooltip.jsx";
 
 import "./LineAreaBarChart.css";
 
@@ -41,7 +40,7 @@ for (let i = 0; i < MAX_SERIES; i++) {
     addCSSRule(`.LineAreaBarChart.mute-${i} svg:not(.stacked) .row`, MUTE_STYLE);
 }
 
-import type { VisualizationProps } from "metabase/visualizations";
+import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 export default class LineAreaBarChart extends Component<*, VisualizationProps, *> {
     static identifier: string;
@@ -176,7 +175,7 @@ export default class LineAreaBarChart extends Component<*, VisualizationProps, *
     }
 
     render() {
-        const { series, hovered, showTitle, actionButtons, linkToCard } = this.props;
+        const { series, hovered, showTitle, actionButtons, linkToCard, onVisualizationClick, visualizationIsClickable } = this.props;
 
         const settings = this.getSettings();
 
@@ -216,6 +215,8 @@ export default class LineAreaBarChart extends Component<*, VisualizationProps, *
                         onHoverChange={this.props.onHoverChange}
                         actionButtons={!titleHeaderSeries ? actionButtons : null}
                         linkToCard={linkToCard}
+                        onVisualizationClick={onVisualizationClick}
+                        visualizationIsClickable={visualizationIsClickable}
                     />
                 : null }
                 <CardRenderer
@@ -226,7 +227,6 @@ export default class LineAreaBarChart extends Component<*, VisualizationProps, *
                     maxSeries={MAX_SERIES}
                     renderer={this.constructor.renderer}
                 />
-                <ChartTooltip series={series} hovered={hovered} />
             </div>
         );
     }
@@ -296,12 +296,18 @@ function transformSingleSeries(s, series, seriesIndex) {
                 ].filter(n => n).join(": "),
                 _transformed: true,
                 _breakoutValue: breakoutValue,
-                _breakoutColumn: cols[seriesColumnIndex]
+                _breakoutColumn: cols[seriesColumnIndex],
             },
             data: {
                 rows: breakoutRowsByValue.get(breakoutValue),
                 cols: rowColumnIndexes.map(i => cols[i]),
                 _rawCols: cols
+            },
+            clicked: {
+                dimensions: [{
+                    value: breakoutValue,
+                    column: cols[seriesColumnIndex]
+                }]
             }
         }));
     } else {
diff --git a/frontend/src/metabase/visualizations/components/PinMap.jsx b/frontend/src/metabase/visualizations/components/PinMap.jsx
index ff434eefc77991d348e5bb278e58b8357e11a03d..596864c978aec3be5776947a4373acafef1068cc 100644
--- a/frontend/src/metabase/visualizations/components/PinMap.jsx
+++ b/frontend/src/metabase/visualizations/components/PinMap.jsx
@@ -13,7 +13,7 @@ import cx from "classnames";
 
 import L from "leaflet";
 
-import type { VisualizationProps } from "metabase/visualizations";
+import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 type Props = VisualizationProps;
 
diff --git a/frontend/src/metabase/visualizations/components/TableInteractive.jsx b/frontend/src/metabase/visualizations/components/TableInteractive.jsx
index 43884f285a214e58c345c91054316ec47be79800..4e33e5d28b67367582839eca374a08741290b841 100644
--- a/frontend/src/metabase/visualizations/components/TableInteractive.jsx
+++ b/frontend/src/metabase/visualizations/components/TableInteractive.jsx
@@ -8,9 +8,7 @@ import "./TableInteractive.css";
 import Icon from "metabase/components/Icon.jsx";
 
 import Value from "metabase/components/Value.jsx";
-import QuickFilterPopover from "metabase/query_builder/components/QuickFilterPopover.jsx";
 
-import MetabaseAnalytics from "metabase/lib/analytics";
 import { capitalize } from "metabase/lib/formatting";
 import { getFriendlyName } from "metabase/visualizations/lib/utils";
 
@@ -26,20 +24,15 @@ const ROW_HEIGHT = 35;
 const MIN_COLUMN_WIDTH = ROW_HEIGHT;
 const RESIZE_HANDLE_WIDTH = 5;
 
-import type { Column } from "metabase/meta/types/Dataset";
-import type { VisualizationProps } from "metabase/visualizations";
+import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 type Props = VisualizationProps & {
     width: number,
     height: number,
     sort: any,
     isPivoted: boolean,
-    cellClickedFn: (number, number) => void,
-    cellIsClickableFn: (number, number) => boolean,
-    setSortFn: (/* TODO */) => void,
 }
 type State = {
-    popover: ?{ rowIndex: number, columnIndex: number },
     columnWidths: number[],
     contentWidths: ?number[]
 }
@@ -69,7 +62,6 @@ export default class TableInteractive extends Component<*, Props, State> {
         super(props);
 
         this.state = {
-            popover: null,
             columnWidths: [],
             contentWidths: null
         };
@@ -79,16 +71,11 @@ export default class TableInteractive extends Component<*, Props, State> {
     static propTypes = {
         data: PropTypes.object.isRequired,
         isPivoted: PropTypes.bool.isRequired,
-        sort: PropTypes.array,
-        setSortFn: PropTypes.func,
-        cellIsClickableFn: PropTypes.func.isRequired,
-        cellClickedFn: PropTypes.func.isRequired
+        sort: PropTypes.array
     };
 
     static defaultProps = {
         isPivoted: false,
-        cellIsClickableFn: () => false,
-        cellClickedFn: () => {}
     };
 
     componentWillMount() {
@@ -137,7 +124,7 @@ export default class TableInteractive extends Component<*, Props, State> {
             contentWidths: null
         });
         this.columnHasResized = {};
-        this.props.onUpdateVisualizationSettings({ "table.column_widths": [] });
+        this.props.onUpdateVisualizationSettings({ "table.column_widths": undefined });
     }
 
     _measure() {
@@ -154,6 +141,8 @@ export default class TableInteractive extends Component<*, Props, State> {
                     return contentWidths[index] + 1; // + 1 to make sure it doen't wrap?
                 } else if (this.state.columnWidths[index]) {
                     return this.state.columnWidths[index];
+                } else {
+                    return 0;
                 }
             } else {
                 return contentWidths[index] + 1;
@@ -221,82 +210,61 @@ export default class TableInteractive extends Component<*, Props, State> {
         setTimeout(() => this.recomputeGridSize(), 1);
     }
 
-    isSortable() {
-        return (this.props.setSortFn !== undefined);
-    }
-
-    setSort(column: Column) {
-        // lets completely delegate this to someone else up the stack :)
-        this.props.setSortFn(column);
-        MetabaseAnalytics.trackEvent('QueryBuilder', 'Set Sort', 'table column');
-    }
-
-    cellClicked(rowIndex: number, columnIndex: number) {
-        this.props.cellClickedFn(rowIndex, columnIndex);
-    }
-
-    popoverFilterClicked(rowIndex: number, columnIndex: number, operator: string) {
-        this.props.cellClickedFn(rowIndex, columnIndex, operator);
-        this.setState({ popover: null });
-    }
-
-    showPopover(rowIndex: number, columnIndex: number) {
-        this.setState({
-            popover: {
-                rowIndex: rowIndex,
-                columnIndex: columnIndex
-            }
-        });
-    }
-
-    onClosePopover = () => {
-        this.setState({ popover: null });
-    }
-
     cellRenderer = ({ key, style, rowIndex, columnIndex }: CellRendererProps) => {
-        const { data: { cols, rows }} = this.props;
+        const { isPivoted, onVisualizationClick, visualizationIsClickable } = this.props;
+        // $FlowFixMe: not sure why flow has a problem with this
+        const { rows, cols } = this.props.data;
+
         const column = cols[columnIndex];
-        const cellData = rows[rowIndex][columnIndex];
-        if (this.props.cellIsClickableFn(rowIndex, columnIndex)) {
-            return (
-                <div
-                    key={key} style={style}
-                    className={cx("TableInteractive-cellWrapper cellData", {
-                        "TableInteractive-cellWrapper--firstColumn": columnIndex === 0
-                    })}
-                    onClick={this.cellClicked.bind(this, rowIndex, columnIndex)}
-                >
-                    <Value className="link" value={cellData} column={column} onResize={this.onCellResize.bind(this, columnIndex)} />
-                </div>
-            );
+        const row = rows[rowIndex];
+        const value = row[columnIndex];
+
+        let clicked;
+        if (isPivoted) {
+            // if it's a pivot table, the first column is
+            if (columnIndex === 0) {
+                clicked = row._dimension;
+            } else {
+                clicked = {
+                    value,
+                    column,
+                    dimensions: [row._dimension, column._dimension]
+                };
+            }
+        } else if (column.source === "aggregation") {
+            clicked = {
+                value,
+                column,
+                dimensions: cols
+                    .map((column, index) => ({ value: row[index], column }))
+                    .filter(dimension => dimension.column.source === "breakout")
+            };
         } else {
-            const { popover } = this.state;
-            const isFilterable = column.source !== "aggregation";
-            return (
-                <div
-                    key={key} style={style}
-                    className={cx("TableInteractive-cellWrapper cellData", {
-                        "TableInteractive-cellWrapper--firstColumn": columnIndex === 0,
-                        "cursor-pointer": isFilterable
-                    })}
-                    onClick={isFilterable && this.showPopover.bind(this, rowIndex, columnIndex)}
-                >
-                    <Value value={cellData} column={column} onResize={this.onCellResize.bind(this, columnIndex)} />
-                    { popover && popover.rowIndex === rowIndex && popover.columnIndex === columnIndex &&
-                        <QuickFilterPopover
-                            column={cols[popover.columnIndex]}
-                            onFilter={this.popoverFilterClicked.bind(this, rowIndex, columnIndex)}
-                            onClose={this.onClosePopover}
-                        />
-                    }
-                </div>
-            );
+            clicked = { value, column };
         }
+
+        const isClickable = onVisualizationClick && visualizationIsClickable(clicked);
+
+        return (
+            <div
+                key={key} style={style}
+                className={cx("TableInteractive-cellWrapper cellData", {
+                    "TableInteractive-cellWrapper--firstColumn": columnIndex === 0,
+                    "cursor-pointer": isClickable
+                })}
+                onClick={isClickable && ((e) => {
+                    onVisualizationClick({ ...clicked, element: e.currentTarget });
+                })}
+            >
+                <Value className="link" value={value} column={column} onResize={this.onCellResize.bind(this, columnIndex)} />
+            </div>
+        );
     }
 
     tableHeaderRenderer = ({ key, style, columnIndex }: CellRendererProps) => {
-        const { sort, data: { cols }} = this.props;
-        const isSortable = this.isSortable();
+        const { sort, isPivoted, onVisualizationClick, visualizationIsClickable } = this.props;
+        // $FlowFixMe: not sure why flow has a problem with this
+        const { cols } = this.props.data;
         const column = cols[columnIndex];
 
         let columnTitle = getFriendlyName(column);
@@ -307,6 +275,19 @@ export default class TableInteractive extends Component<*, Props, State> {
             columnTitle = "Unset";
         }
 
+        let clicked;
+        if (isPivoted) {
+            // if it's a pivot table, the first column is
+            if (columnIndex >= 0) {
+                clicked = column._dimension;
+            }
+        } else {
+            clicked = { column };
+        }
+
+        const isClickable = onVisualizationClick && visualizationIsClickable(clicked);
+        const isSortable = isClickable && column.source;
+
         return (
             <div
                 key={key}
@@ -317,8 +298,10 @@ export default class TableInteractive extends Component<*, Props, State> {
                 })}
             >
                 <div
-                    className={cx("cellData", { "cursor-pointer": isSortable })}
-                    onClick={isSortable && this.setSort.bind(this, column)}
+                    className={cx("cellData", { "cursor-pointer": isClickable })}
+                    onClick={isClickable && ((e) => {
+                        onVisualizationClick({ ...clicked, element: e.currentTarget });
+                    })}
                 >
                     {columnTitle}
                     {isSortable &&
diff --git a/frontend/src/metabase/visualizations/components/TableSimple.jsx b/frontend/src/metabase/visualizations/components/TableSimple.jsx
index 3da9eedcc081d016fff9154b1be777b800fdd7f1..d2f5081be36f05185dd1b424540606134d3bdc47 100644
--- a/frontend/src/metabase/visualizations/components/TableSimple.jsx
+++ b/frontend/src/metabase/visualizations/components/TableSimple.jsx
@@ -14,7 +14,7 @@ import { getFriendlyName } from "metabase/visualizations/lib/utils";
 import cx from "classnames";
 import _ from "underscore";
 
-import type { VisualizationProps } from "metabase/visualizations";
+import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 type Props = VisualizationProps & {
     height: number,
diff --git a/frontend/src/metabase/visualizations/components/Visualization.jsx b/frontend/src/metabase/visualizations/components/Visualization.jsx
index 575c41b592e17badb60887e31567abd44921969a..29d0a5eb9aa79a891983f7b39cec3288c2768541 100644
--- a/frontend/src/metabase/visualizations/components/Visualization.jsx
+++ b/frontend/src/metabase/visualizations/components/Visualization.jsx
@@ -4,6 +4,8 @@ import React, { Component, PropTypes, Element } from "react";
 
 import ExplicitSize from "metabase/components/ExplicitSize.jsx";
 import LegendHeader from "metabase/visualizations/components/LegendHeader.jsx";
+import ChartTooltip from "metabase/visualizations/components/ChartTooltip.jsx";
+import ChartClickActions from "metabase/visualizations/components/ChartClickActions.jsx";
 import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
 import Icon from "metabase/components/Icon.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
@@ -13,19 +15,23 @@ import { duration, formatNumber } from "metabase/lib/formatting";
 import { getVisualizationTransformed } from "metabase/visualizations";
 import { getSettings } from "metabase/visualizations/lib/settings";
 import { isSameSeries } from "metabase/visualizations/lib/utils";
+
 import Utils from "metabase/lib/utils";
+import { datasetContainsNoResults } from "metabase/lib/dataset";
+import { getModeDrills } from "metabase/qb/lib/modes"
 
 import { MinRowsError, ChartSettingsError } from "metabase/visualizations/lib/errors";
 
-import { assoc, getIn, setIn } from "icepick";
+import { assoc, setIn } from "icepick";
 import _ from "underscore";
 import cx from "classnames";
 
 export const ERROR_MESSAGE_GENERIC = "There was a problem displaying this chart.";
 export const ERROR_MESSAGE_PERMISSION = "Sorry, you don't have permission to see this card."
 
-import type { VisualizationSettings } from "metabase/meta/types/Card";
-import type { HoverObject, Series } from "metabase/visualizations";
+import type { Card, VisualizationSettings } from "metabase/meta/types/Card";
+import type { HoverObject, ClickObject, Series, QueryMode } from "metabase/meta/types/Visualization";
+import type { TableMetadata } from "metabase/meta/types/Metadata";
 
 type Props = {
     series: Series,
@@ -53,11 +59,15 @@ type Props = {
     // settings overrides from settings panel
     settings: VisualizationSettings,
 
+    // for click actions
+    mode?: QueryMode,
+    tableMetadata: TableMetadata,
+    onChangeCardAndRun: (card: Card) => void,
+
     // used for showing content in place of visualization, e.x. dashcard filter mapping
     replacementContent: Element<any>,
 
     // used by TableInteractive
-    setSortFn: (any) => void,
     cellIsClickableFn: (number, number) => boolean,
     cellClickedFn: (number, number) => void,
 
@@ -81,6 +91,8 @@ type State = {
     }),
 
     hovered: ?HoverObject,
+    clicked: ?ClickObject,
+
     error: ?Error,
     warnings: string[],
     yAxisSplit: ?number[][],
@@ -96,6 +108,7 @@ export default class Visualization extends Component<*, Props, State> {
 
         this.state = {
             hovered: null,
+            clicked: null,
             error: null,
             warnings: [],
             yAxisSplit: null,
@@ -153,6 +166,7 @@ export default class Visualization extends Component<*, Props, State> {
     transform(newProps) {
         this.setState({
             hovered: null,
+            clicked: null,
             error: null,
             warnings: [],
             yAxisSplit: null,
@@ -160,7 +174,7 @@ export default class Visualization extends Component<*, Props, State> {
         });
     }
 
-    onHoverChange = (hovered) => {
+    handleHoverChange = (hovered) => {
         const { yAxisSplit } = this.state;
         if (hovered) {
             // if we have Y axis split info then find the Y axis index (0 = left, 1 = right)
@@ -172,6 +186,37 @@ export default class Visualization extends Component<*, Props, State> {
         this.setState({ hovered });
     }
 
+    getClickActions(clicked: ?ClickObject) {
+        const { mode, series: [{ card }], tableMetadata } = this.props;
+        return getModeDrills(mode, card, tableMetadata, clicked);
+    }
+
+    visualizationIsClickable = (clicked: ClickObject) => {
+        const { onChangeCardAndRun } = this.props;
+        if (!onChangeCardAndRun) {
+            return false;
+        }
+        try {
+            return this.getClickActions(clicked).length > 0;
+        } catch (e) {
+            return false;
+        }
+    }
+
+    handleVisualizationClick = (clicked: ClickObject) => {
+        // needs to be delayed so we don't clear it when switching from one drill through to another
+        setTimeout(() => {
+            const { onChangeCardAndRun } = this.props;
+            let clickActions = this.getClickActions(clicked);
+            // if there's a single drill action (without a popover) execute it immediately
+            if (clickActions.length === 1 && clickActions[0].default && clickActions[0].card) {
+                onChangeCardAndRun(clickActions[0].card());
+            } else {
+                this.setState({ clicked });
+            }
+        }, 100)
+    }
+
     onRender = ({ yAxisSplit, warnings = [] } = {}) => {
         this.setState({ yAxisSplit, warnings });
     }
@@ -185,6 +230,13 @@ export default class Visualization extends Component<*, Props, State> {
         const { series, CardVisualization } = this.state;
         const small = width < 330;
 
+        let { hovered, clicked } = this.state;
+
+        const clickActions = this.getClickActions(clicked);
+        if (clickActions.length > 0) {
+            hovered = null;
+        }
+
         let error = this.props.error || this.state.error;
         let loading = !(series && series.length > 0 && _.every(series, (s) => s.data));
         let noResults = false;
@@ -222,7 +274,8 @@ export default class Visualization extends Component<*, Props, State> {
         }
 
         if (!error) {
-            noResults = getIn(series, [0, "data", "rows", "length"]) === 0;
+            // $FlowFixMe
+            noResults = series[0] && series[0].data && datasetContainsNoResults(series[0].data);
         }
 
         let extra = (
@@ -320,14 +373,26 @@ export default class Visualization extends Component<*, Props, State> {
                         card={series[0].card} // convienence for single-series visualizations
                         // $FlowFixMe
                         data={series[0].data} // convienence for single-series visualizations
-                        hovered={this.state.hovered}
-                        onHoverChange={this.onHoverChange}
+                        hovered={hovered}
+                        onHoverChange={this.handleHoverChange}
+                        onVisualizationClick={this.handleVisualizationClick}
+                        visualizationIsClickable={this.visualizationIsClickable}
                         onRenderError={this.onRenderError}
                         onRender={this.onRender}
                         gridSize={gridSize}
                         linkToCard={linkToCard}
                     />
                 }
+                <ChartTooltip
+                    series={series}
+                    hovered={hovered}
+                />
+                <ChartClickActions
+                    clicked={clicked}
+                    clickActions={clickActions}
+                    onChangeCardAndRun={this.props.onChangeCardAndRun}
+                    onClose={() => this.setState({ clicked: null })}
+                />
             </div>
         );
     }
diff --git a/frontend/src/metabase/visualizations/index.js b/frontend/src/metabase/visualizations/index.js
index 9e25849f0ae94e9cf60fc3f0eaa20fcc2405e20d..2e9780978050ce64be8c86797c60c999dcffb99b 100644
--- a/frontend/src/metabase/visualizations/index.js
+++ b/frontend/src/metabase/visualizations/index.js
@@ -16,45 +16,7 @@ import Funnel      from "./visualizations/Funnel.jsx";
 
 import _ from "underscore";
 
-import type { DatasetData, Column } from "metabase/meta/types/Dataset";
-import type { Card, VisualizationSettings } from "metabase/meta/types/Card";
-
-export type HoverData = Array<{ key: string, value: any, col?: Column }>;
-
-export type HoverObject = {
-    index?: number,
-    axisIndex?: number,
-    data?: HoverData
-}
-
-// type Visualization = Component<*, VisualizationProps, *>;
-
-// $FlowFixMe
-export type Series = { card: Card, data: DatasetData }[] & { _raw: Series }
-
-export type VisualizationProps = {
-    series: Series,
-    card: Card,
-    data: DatasetData,
-    settings: VisualizationSettings,
-
-    className?: string,
-    gridSize: ?{
-        width: number,
-        height: number
-    },
-
-    showTitle: boolean,
-    isDashboard: boolean,
-    isEditing: boolean,
-    actionButtons: Node,
-    linkToCard?: bool,
-
-    hovered: ?HoverObject,
-    onHoverChange: (?HoverObject) => void,
-
-    onUpdateVisualizationSettings: ({ [key: string]: any }) => void
-}
+import type { Series } from "metabase/meta/types/Visualization";
 
 const visualizations = new Map();
 const aliases = new Map();
diff --git a/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js b/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js
index 76ab119a1acdc3c7f86c56fdf3c82bed55850fab..a2ecd311f47ef29c49de0db216ad4f33c9701925 100644
--- a/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js
+++ b/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js
@@ -293,64 +293,119 @@ function applyChartYAxis(chart, settings, series, yExtent, axisName) {
     }
 }
 
-function applyChartTooltips(chart, series, isStacked, onHoverChange) {
+function applyChartTooltips(chart, series, isStacked, onHoverChange, onVisualizationClick) {
     let [{ data: { cols } }] = series;
     chart.on("renderlet.tooltips", function(chart) {
-        chart.selectAll(".bar, .dot, .area, .line, .bubble")
-            .on("mousemove", function(d, i) {
-                const seriesIndex = determineSeriesIndexFromElement(this, isStacked);
-                const card = series[seriesIndex].card;
-                const isSingleSeriesBar = this.classList.contains("bar") && series.length === 1;
-                const isArea = this.classList.contains("area");
-
-                let data = [];
-                if (Array.isArray(d.key)) { // scatter
-                    if (d.key._origin) {
-                        data = d.key._origin.row.map((value, index) => {
-                            const col = d.key._origin.cols[index];
-                            return { key: getFriendlyName(col), value: value, col };
-                        });
-                    } else {
-                        data = d.key.map((value, index) => (
-                            { key: getFriendlyName(cols[index]), value: value, col: cols[index] }
-                        ));
+        chart.selectAll("title").remove();
+
+        if (onHoverChange) {
+            chart.selectAll(".bar, .dot, .area, .line, .bubble")
+                .on("mousemove", function(d, i) {
+                    const seriesIndex = determineSeriesIndexFromElement(this, isStacked);
+                    const card = series[seriesIndex].card;
+                    const isSingleSeriesBar = this.classList.contains("bar") && series.length === 1;
+                    const isArea = this.classList.contains("area");
+
+                    let data = [];
+                    if (Array.isArray(d.key)) { // scatter
+                        if (d.key._origin) {
+                            data = d.key._origin.row.map((value, index) => {
+                                const col = d.key._origin.cols[index];
+                                return { key: getFriendlyName(col), value: value, col };
+                            });
+                        } else {
+                            data = d.key.map((value, index) => (
+                                { key: getFriendlyName(cols[index]), value: value, col: cols[index] }
+                            ));
+                        }
+                    } else if (d.data) { // line, area, bar
+                        if (!isSingleSeriesBar) {
+                            cols = series[seriesIndex].data.cols;
+                        }
+                        data = [
+                            { key: getFriendlyName(cols[0]), value: d.data.key, col: cols[0] },
+                            { key: getFriendlyName(cols[1]), value: d.data.value, col: cols[1] }
+                        ];
                     }
-                } else if (d.data) { // line, area, bar
-                    if (!isSingleSeriesBar) {
-                        cols = series[seriesIndex].data.cols;
+
+                    if (data && series.length > 1) {
+                        if (card._breakoutColumn) {
+                            data.unshift({
+                                key: getFriendlyName(card._breakoutColumn),
+                                value: card._breakoutValue,
+                                col: card._breakoutColumn
+                            });
+                        }
                     }
-                    data = [
-                        { key: getFriendlyName(cols[0]), value: d.data.key, col: cols[0] },
-                        { key: getFriendlyName(cols[1]), value: d.data.value, col: cols[1] }
-                    ];
-                }
 
-                if (data && series.length > 1) {
-                    if (card._breakoutColumn) {
-                        data.unshift({
-                            key: getFriendlyName(card._breakoutColumn),
+                    data = _.uniq(data, (d) => d.col);
+
+                    onHoverChange({
+                        // for single series bar charts, fade the series and highlght the hovered element with CSS
+                        index: isSingleSeriesBar ? -1 : seriesIndex,
+                        // for area charts, use the mouse location rather than the DOM element
+                        element: isArea ? null : this,
+                        event: isArea ? d3.event : null,
+                        data: data.length > 0 ? data : null,
+                    });
+                })
+                .on("mouseleave", function() {
+                    if (!onHoverChange) {
+                        return;
+                    }
+                    onHoverChange(null);
+                })
+        }
+
+        if (onVisualizationClick) {
+            chart.selectAll(".bar, .dot, .bubble")
+                .style({ "cursor": "pointer" })
+                .on("mouseup", function(d) {
+                    const seriesIndex = determineSeriesIndexFromElement(this, isStacked);
+                    const card = series[seriesIndex].card;
+                    const isSingleSeriesBar = this.classList.contains("bar") && series.length === 1;
+
+                    let clicked;
+                    if (Array.isArray(d.key)) { // scatter
+                        clicked = {
+                            value: d.key[2],
+                            column: cols[2],
+                            dimensions: [
+                                { value: d.key[0], column: cols[0] },
+                                { value: d.key[1], column: cols[1] }
+                            ],
+                            origin: d.key._origin
+                        }
+                    } else if (d.data) { // line, area, bar
+                        if (!isSingleSeriesBar) {
+                            cols = series[seriesIndex].data.cols;
+                        }
+                        clicked = {
+                            value: d.data.value,
+                            column: cols[1],
+                            dimensions: [
+                                { value: d.data.key, column: cols[0] }
+                            ]
+                        }
+                    }
+
+                    if (clicked && series.length > 1 && card._breakoutColumn) {
+                        clicked.dimensions.push({
                             value: card._breakoutValue,
-                            col: card._breakoutColumn
+                            column: card._breakoutColumn
                         });
                     }
-                }
-
-                data = _.uniq(data, (d) => d.col);
 
-                onHoverChange && onHoverChange({
-                    // for single series bar charts, fade the series and highlght the hovered element with CSS
-                    index: isSingleSeriesBar ? -1 : seriesIndex,
-                    // for area charts, use the mouse location rather than the DOM element
-                    element: isArea ? null : this,
-                    event: isArea ? d3.event : null,
-                    data: data.length > 0 ? data : null,
+                    if (clicked) {
+                        const isLine = this.classList.contains("dot");
+                        onVisualizationClick({
+                            ...clicked,
+                            element: isLine ? this : null,
+                            event: isLine ? null : d3.event,
+                        });
+                    }
                 });
-            })
-            .on("mouseleave", function() {
-                onHoverChange && onHoverChange(null);
-            });
-
-        chart.selectAll("title").remove();
+        }
     });
 }
 
@@ -487,6 +542,10 @@ function lineAndBarOnRender(chart, settings, onGoalHover, isSplitAxis, isStacked
                         dispatchUIEvent(e, "mouseleave");
                         d3.select(e).classed("hover", false);
                     })
+                    .on("mouseup", ({ point }) => {
+                        let e = point[2];
+                        dispatchUIEvent(e, "mouseup");
+                    })
                 .order();
 
         function dispatchUIEvent(element, eventName) {
@@ -746,7 +805,7 @@ function forceSortedGroupsOfGroups(groupsOfGroups: CrossfilterGroup[][], indexMa
 }
 
 
-export default function lineAreaBar(element, { series, onHoverChange, onRender, chartType, isScalarSeries, settings, maxSeries }) {
+export default function lineAreaBar(element, { series, onHoverChange, onVisualizationClick, onRender, chartType, isScalarSeries, settings, maxSeries }) {
     const colors = settings["graph.colors"];
 
     const isTimeseries = settings["graph.x_axis.scale"] === "timeseries";
@@ -1073,7 +1132,7 @@ export default function lineAreaBar(element, { series, onHoverChange, onRender,
             }
             onHoverChange(hovered);
         }
-    });
+    }, onVisualizationClick);
 
     // render
     parent.render();
@@ -1101,10 +1160,19 @@ export const scatterRenderer = (element, props) => lineAreaBar(element, { ...pro
 
 export function rowRenderer(
   element,
-  { settings, series, onHoverChange, height }
+  { settings, series, onHoverChange, onVisualizationClick, height }
 ) {
+  const { cols } = series[0].data;
+
+  if (series.length > 1) {
+    throw new Error("Row chart does not support multiple series");
+  }
+
   const chart = dc.rowChart(element);
 
+  // disable clicks
+  chart.onClick = () => {};
+
   const colors = settings["graph.colors"];
 
   const dataset = crossfilter(series[0].data.rows);
@@ -1118,20 +1186,35 @@ export function rowRenderer(
   initChart(chart, element);
 
   chart.on("renderlet.tooltips", chart => {
-    chart.selectAll(".row rect").on("mousemove", (d, i) => {
-      const { cols } = series[0].data;
-      onHoverChange && onHoverChange({
-          // for single series bar charts, fade the series and highlght the hovered element with CSS
-          index: -1,
-          event: d3.event,
-          data: [
-            { key: getFriendlyName(cols[0]), value: d.key, col: cols[0] },
-            { key: getFriendlyName(cols[1]), value: d.value, col: cols[1] }
-          ]
-        });
-    }).on("mouseleave", () => {
-      onHoverChange && onHoverChange(null);
-    });
+      if (onHoverChange) {
+          chart.selectAll(".row rect").on("mousemove", (d, i) => {
+            onHoverChange && onHoverChange({
+                // for single series bar charts, fade the series and highlght the hovered element with CSS
+                index: -1,
+                event: d3.event,
+                data: [
+                  { key: getFriendlyName(cols[0]), value: d.key, col: cols[0] },
+                  { key: getFriendlyName(cols[1]), value: d.value, col: cols[1] }
+                ]
+              });
+          }).on("mouseleave", () => {
+            onHoverChange && onHoverChange(null);
+          });
+      }
+
+      if (onVisualizationClick) {
+          chart.selectAll(".row rect").on("mouseup", function(d) {
+              onVisualizationClick({
+                  value: d.value,
+                  column: cols[1],
+                  dimensions: [{
+                      value: d.key,
+                      column: cols[0]
+                  }],
+                  element: this
+              })
+          });
+      }
   });
 
   chart
diff --git a/frontend/src/metabase/visualizations/visualizations/Funnel.jsx b/frontend/src/metabase/visualizations/visualizations/Funnel.jsx
index c68b58e63828a52aa9987ebd37dc71e451eaca64..4fd4fb8ef028bf4a1527415d56701387613a9fb5 100644
--- a/frontend/src/metabase/visualizations/visualizations/Funnel.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Funnel.jsx
@@ -15,7 +15,7 @@ import LegendHeader from "../components/LegendHeader";
 import _ from "underscore";
 import cx from "classnames";
 
-import type { VisualizationProps } from "..";
+import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 export default class Funnel extends Component<*, VisualizationProps, *> {
     static uiName = "Funnel";
diff --git a/frontend/src/metabase/visualizations/visualizations/Map.jsx b/frontend/src/metabase/visualizations/visualizations/Map.jsx
index a1a32dee10936b6354b7c1d2ee908f4a2d143210..d552cc87a7f73032b44ff851b3bf9b78b6ae5d9e 100644
--- a/frontend/src/metabase/visualizations/visualizations/Map.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Map.jsx
@@ -10,7 +10,7 @@ import { isNumeric, isLatitude, isLongitude, hasLatitudeAndLongitudeColumns } fr
 import { metricSetting, dimensionSetting, fieldSetting } from "metabase/visualizations/lib/settings";
 import MetabaseSettings from "metabase/lib/settings";
 
-import type { VisualizationProps } from "metabase/visualizations";
+import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 import _ from "underscore";
 
diff --git a/frontend/src/metabase/visualizations/visualizations/PieChart.jsx b/frontend/src/metabase/visualizations/visualizations/PieChart.jsx
index 0efb2e80edf190ac1513a3a5e70d0b567aff3141..1e4420e735f70313604df9b09944eda2e0680887 100644
--- a/frontend/src/metabase/visualizations/visualizations/PieChart.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/PieChart.jsx
@@ -29,7 +29,7 @@ const OTHER_SLICE_MIN_PERCENTAGE = 0.003;
 
 const PERCENT_REGEX = /percent/i;
 
-import type { VisualizationProps } from "metabase/visualizations";
+import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 type Props = VisualizationProps;
 
@@ -90,7 +90,7 @@ export default class PieChart extends Component<*, Props, *> {
     }
 
     render() {
-        const { series, hovered, onHoverChange, className, gridSize, settings } = this.props;
+        const { series, hovered, onHoverChange, onVisualizationClick, className, gridSize, settings } = this.props;
 
         const [{ data: { cols, rows }}] = series;
         const dimensionIndex = _.findIndex(cols, (col) => col.name === settings["pie.dimension"]);
@@ -154,7 +154,7 @@ export default class PieChart extends Component<*, Props, *> {
             .outerRadius(OUTER_RADIUS)
             .innerRadius(OUTER_RADIUS * INNER_RADIUS_RATIO);
 
-        let hoverForIndex = (index, event) => ({
+        const hoverForIndex = (index, event) => ({
             index,
             event: event && event.nativeEvent,
             data: slices[index] === otherSlice ?
@@ -168,10 +168,24 @@ export default class PieChart extends Component<*, Props, *> {
             ].concat(showPercentInTooltip ? [{ key: "Percentage", value: formatPercent(slices[index].percentage) }] : [])
         });
 
+        const onClickSlice = ({ index, event }) => {
+            if (onVisualizationClick && slices[index] !== otherSlice) {
+                onVisualizationClick({
+                    value:  slices[index].value,
+                    column: cols[metricIndex],
+                    dimensions: [{
+                        value: slices[index].key,
+                        column: cols[dimensionIndex],
+                    }],
+                    event:        event
+                })
+            }
+        }
+
         let value, title;
         if (hovered && hovered.index != null && slices[hovered.index] !== otherSlice) {
-            title = slices[hovered.index].key;
-            value = slices[hovered.index].value;
+            title = formatDimension(slices[hovered.index].key);
+            value = formatMetric(slices[hovered.index].value);
         } else {
             title = "Total";
             value = formatMetric(total);
@@ -201,6 +215,10 @@ export default class PieChart extends Component<*, Props, *> {
                                         opacity={(hovered && hovered.index != null && hovered.index !== index) ? 0.3 : 1}
                                         onMouseMove={(e) => onHoverChange && onHoverChange(hoverForIndex(index, e))}
                                         onMouseLeave={() => onHoverChange && onHoverChange(null)}
+                                        onClick={(e) => onClickSlice({
+                                            index: index,
+                                            event: e.nativeEvent
+                                        })}
                                     />
                                 )}
                             </g>
diff --git a/frontend/src/metabase/visualizations/visualizations/Progress.jsx b/frontend/src/metabase/visualizations/visualizations/Progress.jsx
index 15ae1c1c09029503b613ec9ebec85fa16c3930fc..1189f5d1afcf797b275fcd31bcb3d34e8f33c9cf 100644
--- a/frontend/src/metabase/visualizations/visualizations/Progress.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Progress.jsx
@@ -15,7 +15,7 @@ import cx from "classnames";
 const BORDER_RADIUS = 5;
 const MAX_BAR_HEIGHT = 65;
 
-import type { VisualizationProps } from "metabase/visualizations";
+import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 export default class Progress extends Component<*, VisualizationProps, *> {
     static uiName = "Progress";
@@ -102,7 +102,7 @@ export default class Progress extends Component<*, VisualizationProps, *> {
     }
 
     render() {
-        const { series: [{ data: { rows } }], settings } = this.props;
+        const { series: [{ data: { rows, cols } }], settings, onVisualizationClick, visualizationIsClickable } = this.props;
         const value: number = rows[0][0];
         const goal = settings["progress.goal"] || 0;
 
@@ -124,6 +124,12 @@ export default class Progress extends Component<*, VisualizationProps, *> {
             barMessage = "Goal exceeded";
         }
 
+        const clicked = {
+            value: value,
+            column: cols[0]
+        };
+        const isClickable = visualizationIsClickable(clicked);
+
         return (
             <div className={cx(this.props.className, "flex layout-centered")}>
                 <div className="flex-full full-height flex flex-column justify-center" style={{ padding: 10, paddingTop: 0 }}>
@@ -154,11 +160,16 @@ export default class Progress extends Component<*, VisualizationProps, *> {
                             }}
                         />
                     </div>
-                    <div ref="bar" className="relative" style={{
-                        backgroundColor: restColor,
-                        borderRadius: BORDER_RADIUS,
-                        overflow: "hidden"
-                    }}>
+                    <div
+                        ref="bar"
+                        className={cx("relative", { "cursor-pointer": isClickable })}
+                        style={{
+                            backgroundColor: restColor,
+                            borderRadius: BORDER_RADIUS,
+                            overflow: "hidden"
+                        }}
+                        onClick={isClickable && ((e) => onVisualizationClick({ ...clicked, event: e.nativeEvent }))}
+                    >
                         <div style={{
                                 backgroundColor: progressColor,
                                 width: (barPercent * 100) + "%",
diff --git a/frontend/src/metabase/visualizations/visualizations/Scalar.css b/frontend/src/metabase/visualizations/visualizations/Scalar.css
index b4f60531862f850b77f7f33390aac62da629789d..0a372b305caf7145fbcf067662a3931b255e36f0 100644
--- a/frontend/src/metabase/visualizations/visualizations/Scalar.css
+++ b/frontend/src/metabase/visualizations/visualizations/Scalar.css
@@ -6,7 +6,6 @@
   color: #525658;
 }
 :local .Scalar .Value {
-  color: rgb(31,31,31);
   font-weight: bold;
 }
 :local .Scalar .Title {
diff --git a/frontend/src/metabase/visualizations/visualizations/Scalar.jsx b/frontend/src/metabase/visualizations/visualizations/Scalar.jsx
index fb37b7cb882cf279fb6f45845fbf6159be3f30bd..0109394d22e216b61bba2ae5395b0ce4892f1645 100644
--- a/frontend/src/metabase/visualizations/visualizations/Scalar.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Scalar.jsx
@@ -8,16 +8,15 @@ import Icon from "metabase/components/Icon.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
 import Ellipsified from "metabase/components/Ellipsified.jsx";
 
-import Urls from "metabase/lib/urls";
+import * as Urls from "metabase/lib/urls";
 import { formatValue } from "metabase/lib/formatting";
 import { TYPE } from "metabase/lib/types";
 import { isNumber } from "metabase/lib/schema_metadata";
 
 import cx from "classnames";
-import { getIn } from "icepick";
 import d3 from "d3";
 
-import type { VisualizationProps } from "metabase/visualizations";
+import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 export default class Scalar extends Component<*, VisualizationProps, *> {
     static uiName = "Number";
@@ -29,6 +28,8 @@ export default class Scalar extends Component<*, VisualizationProps, *> {
 
     static minSize = { width: 3, height: 3 };
 
+    _scalar: ?HTMLElement;
+
     static isSensible(cols, rows) {
         return rows.length === 1 && cols.length === 1;
     }
@@ -102,13 +103,13 @@ export default class Scalar extends Component<*, VisualizationProps, *> {
     };
 
     render() {
-        let { card, data, className, actionButtons, gridSize, settings, linkToCard } = this.props;
+        let { series: [{ card, data: { cols, rows }}], className, actionButtons, gridSize, settings, linkToCard, visualizationIsClickable, onVisualizationClick } = this.props;
         let description = settings["card.description"];
 
         let isSmall = gridSize && gridSize.width < 4;
-        const column = getIn(data, ["cols", 0]);
+        const column = cols[0];
 
-        let scalarValue = getIn(data, ["rows", 0, 0]);
+        let scalarValue = rows[0] && rows[0][0];
         if (scalarValue == null) {
             scalarValue = "";
         }
@@ -167,21 +168,34 @@ export default class Scalar extends Component<*, VisualizationProps, *> {
             fullScalarValue = fullScalarValue + settings["scalar.suffix"];
         }
 
+        const clicked = {
+            value: rows[0] && rows[0][0],
+            column: cols[0]
+        };
+        const isClickable = visualizationIsClickable(clicked);
+
         return (
             <div className={cx(className, styles.Scalar, styles[isSmall ? "small" : "large"])}>
                 <div className="Card-title absolute top right p1 px2">{actionButtons}</div>
                 <Ellipsified
-                    className={cx(styles.Value, 'ScalarValue', 'fullscreen-normal-text', 'fullscreen-night-text')}
+                    className={cx(styles.Value, 'ScalarValue text-dark fullscreen-normal-text fullscreen-night-text', {
+                        "text-brand-hover cursor-pointer": isClickable
+                    })}
                     tooltip={fullScalarValue}
                     alwaysShowTooltip={fullScalarValue !== compactScalarValue}
                     style={{maxWidth: '100%'}}
                 >
-                    {compactScalarValue}
+                    <span
+                        onClick={isClickable && (() => this._scalar && onVisualizationClick({ ...clicked, element: this._scalar }))}
+                        ref={scalar => this._scalar = scalar}
+                    >
+                        {compactScalarValue}
+                    </span>
                 </Ellipsified>
                 <div className={styles.Title + " flex align-center"}>
                     <Ellipsified tooltip={card.name}>
                         { linkToCard ?
-                          <Link to={Urls.card(card.id)} className="no-decoration fullscreen-normal-text fullscreen-night-text">{settings["card.title"]}</Link>
+                          <Link to={Urls.question(card.id)} className="no-decoration fullscreen-normal-text fullscreen-night-text">{settings["card.title"]}</Link>
                           :
                           <span className="fullscreen-normal-text fullscreen-night-text">{settings["card.title"]}</span>
                         }
diff --git a/frontend/src/metabase/visualizations/visualizations/Table.jsx b/frontend/src/metabase/visualizations/visualizations/Table.jsx
index 1b186d023a2bfeffece08921a98e00e94834f31a..bd9477488b93f34145278e3f5ddb0e23f4e34bfc 100644
--- a/frontend/src/metabase/visualizations/visualizations/Table.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Table.jsx
@@ -24,13 +24,9 @@ type Props = {
     data: DatasetData,
     settings: VisualizationSettings,
     isDashboard: boolean,
-    cellClickedFn: (number, number) => void,
-    cellIsClickableFn: (number, number) => boolean,
-    setSortFn: (/* TODO */) => void,
 }
 type State = {
-    data: ?DatasetData,
-    columnIndexes: number[]
+    data: ?DatasetData
 }
 
 export default class Table extends Component<*, Props, State> {
@@ -87,8 +83,7 @@ export default class Table extends Component<*, Props, State> {
         super(props);
 
         this.state = {
-            data: null,
-            columnIndexes: []
+            data: null
         };
     }
 
@@ -103,14 +98,6 @@ export default class Table extends Component<*, Props, State> {
         }
     }
 
-    cellClicked = (rowIndex: number, columnIndex: number, ...args: any[]) => {
-        this.props.cellClickedFn(rowIndex, this.state.columnIndexes[columnIndex], ...args);
-    }
-
-    cellIsClickable = (rowIndex: number, columnIndex: number, ...args: any[]) => {
-        return this.props.cellIsClickableFn(rowIndex, this.state.columnIndexes[columnIndex], ...args);
-    }
-
     _updateData({ data, settings }: { data: DatasetData, settings: VisualizationSettings }) {
         if (settings["table.pivot"]) {
             this.setState({
@@ -128,14 +115,13 @@ export default class Table extends Component<*, Props, State> {
                     cols: columnIndexes.map(i => cols[i]),
                     columns: columnIndexes.map(i => columns[i]),
                     rows: rows.map(row => columnIndexes.map(i => row[i]))
-                },
-                columnIndexes
+                }
             });
         }
     }
 
     render() {
-        const { card, cellClickedFn, cellIsClickableFn, setSortFn, isDashboard, settings } = this.props;
+        const { card, isDashboard, settings } = this.props;
         const { data } = this.state;
         const sort = getIn(card, ["dataset_query", "query", "order_by"]) || null;
         const isPivoted = settings["table.pivot"];
@@ -146,9 +132,6 @@ export default class Table extends Component<*, Props, State> {
                 data={data}
                 isPivoted={isPivoted}
                 sort={sort}
-                setSortFn={isPivoted ? undefined : setSortFn}
-                cellClickedFn={(!cellClickedFn || isPivoted) ? undefined : this.cellClicked}
-                cellIsClickableFn={(!cellIsClickableFn || isPivoted) ? undefined : this.cellIsClickable}
             />
         );
     }
diff --git a/frontend/test/e2e/auth/login.spec.js b/frontend/test/e2e/auth/login.spec.js
index d67f0476d4aff0f55317f1b3a5c71743fda8e77e..fe191cebf9c5107762286b3346a5890b20aed86a 100644
--- a/frontend/test/e2e/auth/login.spec.js
+++ b/frontend/test/e2e/auth/login.spec.js
@@ -55,8 +55,8 @@ describeE2E("auth/login", () => {
         });
 
         it("loads the qb", async () => {
-            await driver.get(`${server.host}/q#eyJuYW1lIjpudWxsLCJkYXRhc2V0X3F1ZXJ5Ijp7ImRhdGFiYXNlIjoxLCJ0eXBlIjoibmF0aXZlIiwibmF0aXZlIjp7InF1ZXJ5Ijoic2VsZWN0ICdvaCBoYWkgZ3Vpc2Ug8J-QsScifSwicGFyYW1ldGVycyI6W119LCJkaXNwbGF5Ijoic2NhbGFyIiwidmlzdWFsaXphdGlvbl9zZXR0aW5ncyI6e319`);
-            await waitForUrl(driver, `${server.host}/q#eyJuYW1lIjpudWxsLCJkYXRhc2V0X3F1ZXJ5Ijp7ImRhdGFiYXNlIjoxLCJ0eXBlIjoibmF0aXZlIiwibmF0aXZlIjp7InF1ZXJ5Ijoic2VsZWN0ICdvaCBoYWkgZ3Vpc2Ug8J-QsScifSwicGFyYW1ldGVycyI6W119LCJkaXNwbGF5Ijoic2NhbGFyIiwidmlzdWFsaXphdGlvbl9zZXR0aW5ncyI6e319`);
+            await driver.get(`${server.host}/question#eyJuYW1lIjpudWxsLCJkYXRhc2V0X3F1ZXJ5Ijp7ImRhdGFiYXNlIjoxLCJ0eXBlIjoibmF0aXZlIiwibmF0aXZlIjp7InF1ZXJ5Ijoic2VsZWN0ICdvaCBoYWkgZ3Vpc2Ug8J-QsScifSwicGFyYW1ldGVycyI6W119LCJkaXNwbGF5Ijoic2NhbGFyIiwidmlzdWFsaXphdGlvbl9zZXR0aW5ncyI6e319`);
+            await waitForUrl(driver, `${server.host}/question#eyJuYW1lIjpudWxsLCJkYXRhc2V0X3F1ZXJ5Ijp7ImRhdGFiYXNlIjoxLCJ0eXBlIjoibmF0aXZlIiwibmF0aXZlIjp7InF1ZXJ5Ijoic2VsZWN0ICdvaCBoYWkgZ3Vpc2Ug8J-QsScifSwicGFyYW1ldGVycyI6W119LCJkaXNwbGF5Ijoic2NhbGFyIiwidmlzdWFsaXphdGlvbl9zZXR0aW5ncyI6e319`);
             await screenshot(driver, "screenshots/qb.png");
         });
     });
diff --git a/frontend/test/e2e/query_builder/query_builder.spec.js b/frontend/test/e2e/query_builder/query_builder.spec.js
index 6e4106bd8722b4ed842020f59468a80d715e3e2a..9eb6aa8d69d39a6dc0d9d572ca0f2113db7db8ee 100644
--- a/frontend/test/e2e/query_builder/query_builder.spec.js
+++ b/frontend/test/e2e/query_builder/query_builder.spec.js
@@ -15,7 +15,7 @@ describeE2E("query_builder", () => {
     describe("tables", () => {
         it("should allow users to create pivot tables", async () => {
             // load the query builder and screenshot blank
-            await d.get("/q");
+            await d.get("/question");
             await d.screenshot("screenshots/qb-initial.png");
 
             // pick the orders table (assumes database is already selected, i.e. there's only 1 database)
@@ -56,7 +56,7 @@ describeE2E("query_builder", () => {
 
     describe("charts", () => {
         xit("should allow users to create line charts", async () => {
-            await d.get("/q");
+            await d.get("/question");
 
             // select orders table
             await d.select("#TablePicker .List-item:first-child>a").wait().click();
@@ -106,7 +106,7 @@ describeE2E("query_builder", () => {
 
         xit("should allow users to create bar charts", async () => {
             // load line chart
-            await d.get("/card/2");
+            await d.get("/question/2");
 
             // dismiss saved questions modal
             await d.select(".Modal .Button.Button--primary").wait().click();
diff --git a/frontend/test/e2e/query_builder/tutorial.spec.js b/frontend/test/e2e/query_builder/tutorial.spec.js
index 8b6b31f589a2becb8170a0bac563025c3ad08501..9298adaf0a18211a64e7647e82286bf14ddf5504 100644
--- a/frontend/test/e2e/query_builder/tutorial.spec.js
+++ b/frontend/test/e2e/query_builder/tutorial.spec.js
@@ -24,7 +24,7 @@ describeE2E("tutorial", () => {
         await waitForElementAndClick(driver, ".Modal .Button.Button--primary");
         await waitForElementAndClick(driver, ".Modal .Button.Button--primary");
 
-        await waitForUrl(driver, `${server.host}/q`);
+        await waitForUrl(driver, `${server.host}/question`);
         await waitForElement(driver, ".Modal .Button.Button--primary");
         await screenshot(driver, "screenshots/setup-tutorial-qb.png");
         await waitForElementAndClick(driver, ".Modal .Button.Button--primary");
diff --git a/frontend/test/unit/lib/data_grid.spec.js b/frontend/test/unit/lib/data_grid.spec.js
index 0ad32a2f31dc007c9a42bff4f61553939db19abb..fd968a0cbd64e53a57ea0d65643ae990cb3adb52 100644
--- a/frontend/test/unit/lib/data_grid.spec.js
+++ b/frontend/test/unit/lib/data_grid.spec.js
@@ -27,7 +27,7 @@ describe("data_grid", () => {
             ])
             let pivotedData = pivot(data);
             expect(pivotedData.cols.length).toEqual(3);
-            expect(pivotedData.rows).toEqual([
+            expect(pivotedData.rows.map(row => [...row])).toEqual([
                 ["x", 1, 4],
                 ["y", 2, 5],
                 ["z", 3, 6]
diff --git a/package.json b/package.json
index ace8b699ed34df85e90cb1be03d9f6741621b91b..77edb5a62183b55dc27d90deac3fb94b2cfdd11f 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
     "classnames": "^2.1.3",
     "color": "^1.0.3",
     "crossfilter": "^1.3.12",
+    "cxs": "^3.0.4",
     "d3": "^3.5.17",
     "dc": "^2.0.0",
     "diff": "^3.2.0",
@@ -101,7 +102,9 @@
     "flow-bin": "^0.37.4",
     "fs-promise": "^1.0.0",
     "glob": "^7.1.1",
+    "html-webpack-harddisk-plugin": "^0.1.0",
     "html-webpack-plugin": "^2.14.0",
+    "husky": "^0.13.2",
     "image-diff": "^1.6.3",
     "imports-loader": "^0.7.0",
     "jasmine": "^2.4.1",
@@ -118,11 +121,13 @@
     "karma-junit-reporter": "^1.1.0",
     "karma-nyan-reporter": "^0.2.2",
     "karma-webpack": "^1.7.0",
+    "lint-staged": "^3.3.1",
     "loader-utils": "^0.2.12",
     "postcss-cssnext": "^2.4.0",
     "postcss-import": "^9.0.0",
     "postcss-loader": "^1.2.1",
     "postcss-url": "^5.1.1",
+    "prettier": "^0.21.0",
     "promise-loader": "^1.0.0",
     "react-addons-test-utils": "^15.4.2",
     "react-hot-loader": "^1.3.0",
@@ -139,7 +144,9 @@
   },
   "scripts": {
     "dev": "yarn && concurrently --kill-others -p name -n 'backend,frontend' -c 'blue,green' 'lein ring server' 'yarn run build-hot'",
-    "lint": "eslint --ext .js --ext .jsx --max-warnings 0 frontend/src frontend/test",
+    "lint": "yarn run lint-eslint && yarn run lint-prettier",
+    "lint-eslint": "eslint --ext .js --ext .jsx --max-warnings 0 frontend/src frontend/test",
+    "lint-prettier": "prettier --tab-width 4 -l 'frontend/src/metabase/qb/**/*.js*' 'frontend/src/metabase/new_question/**/*.js*' || (echo '\nThese files are not formatted correctly. Did you forget to run \"yarn run prettier\"?' && false)",
     "flow": "flow check",
     "test": "karma start frontend/test/karma.conf.js --single-run",
     "test-watch": "karma start frontend/test/karma.conf.js --auto-watch --reporters nyan",
@@ -147,12 +154,24 @@
     "test-e2e-sauce": "USE_SAUCE=true yarn run test-e2e",
     "build": "webpack --bail",
     "build-watch": "webpack --watch",
-    "build-hot": "NODE_ENV=hot webpack --bail && NODE_ENV=hot webpack-dev-server --progress",
+    "build-hot": "NODE_ENV=hot webpack-dev-server --progress",
     "start": "yarn run build && lein ring server",
     "storybook": "start-storybook -p 9001",
+    "precommit": "lint-staged",
     "preinstall": "echo $npm_execpath | grep -q yarn || echo '\\033[0;33mSorry, npm is not supported. Please use Yarn (https://yarnpkg.com/).\\033[0m'",
+    "prettier": "prettier --tab-width 4 --write 'frontend/src/metabase/qb/**/*.js*' 'frontend/src/metabase/new_question/**/*.js*'",
     "test-jest": "jest"
   },
+  "lint-staged": {
+    "frontend/src/metabase/qb/**/*.js*": [
+      "prettier --tab-width 4 --write",
+      "git add"
+    ],
+    "frontend/src/metabase/new_question/**/*.js*": [
+      "prettier --tab-width 4 --write",
+      "git add"
+    ]
+  },
   "jest": {
     "testPathIgnorePatterns": [
       "<rootDir>/frontend/test/"
diff --git a/project.clj b/project.clj
index 0a2bcb013c9eec78e5014ad2ed3fd447a6aadb5c..f972daf60dc0175ebe9ae6ca40f91f24c996eab1 100644
--- a/project.clj
+++ b/project.clj
@@ -12,7 +12,7 @@
             "profile" ["with-profile" "+profile" "run" "profile"]
             "h2" ["with-profile" "+h2-shell" "run" "-url" "jdbc:h2:./metabase.db" "-user" "" "-password" "" "-driver" "org.h2.Driver"]}
   :dependencies [[org.clojure/clojure "1.8.0"]
-                 [org.clojure/core.async "0.2.395"]
+                 [org.clojure/core.async "0.3.442"]
                  [org.clojure/core.match "0.3.0-alpha4"]              ; optimized pattern matching library for Clojure
                  [org.clojure/core.memoize "0.5.9"]                   ; needed by core.match; has useful FIFO, LRU, etc. caching mechanisms
                  [org.clojure/data.csv "0.1.3"]                       ; CSV parsing / generation
@@ -25,9 +25,9 @@
                   :exclusions [org.clojure/clojure
                                org.clojure/clojurescript]]            ; fixed length queue implementation, used in log buffering
                  [amalloy/ring-gzip-middleware "0.1.3"]               ; Ring middleware to GZIP responses if client can handle it
-                 [aleph "0.4.1"]                                      ; Async HTTP library; WebSockets
+                 [aleph "0.4.3"]                                      ; Async HTTP library; WebSockets
                  [buddy/buddy-core "1.2.0"]                           ; various cryptograhpic functions
-                 [buddy/buddy-sign "1.1.0"]                           ; JSON Web Tokens; High-Level message signing library
+                 [buddy/buddy-sign "1.5.0"]                           ; JSON Web Tokens; High-Level message signing library
                  [cheshire "5.7.0"]                                   ; fast JSON encoding (used by Ring JSON middleware)
                  [clj-http "3.4.1"                                    ; HTTP client
                   :exclusions [commons-codec
@@ -43,13 +43,14 @@
                                ring/ring-core]]
                  [com.draines/postal "2.0.2"]                         ; SMTP library
                  [com.google.apis/google-api-services-analytics       ; Google Analytics Java Client Library
-                  "v3-rev136-1.22.0"]
+                  "v3-rev139-1.22.0"]
                  [com.google.apis/google-api-services-bigquery        ; Google BigQuery Java Client Library
-                  "v2-rev334-1.22.0"]
-                 [com.h2database/h2 "1.4.193"]                        ; embedded SQL database
+                  "v2-rev342-1.22.0"]
+                 [com.h2database/h2 "1.4.194"]                        ; embedded SQL database
                  [com.mattbertolini/liquibase-slf4j "2.0.0"]          ; Java Migrations lib
                  [com.mchange/c3p0 "0.9.5.2"]                         ; connection pooling library
                  [com.novemberain/monger "3.1.0"]                     ; MongoDB Driver
+                 [com.taoensso/nippy "2.13.0"]                        ; Fast serialization (i.e., GZIP) library for Clojure
                  [compojure "1.5.2"]                                  ; HTTP Routing library built on Ring
                  [crypto-random "1.2.0"]                              ; library for generating cryptographically secure random bytes and strings
                  [environ "1.1.0"]                                    ; easy environment management
@@ -67,20 +68,20 @@
                   :exclusions [org.slf4j/slf4j-api]]
                  [net.sourceforge.jtds/jtds "1.3.1"]                  ; Open Source SQL Server driver
                  [org.liquibase/liquibase-core "3.5.3"]               ; migration management (Java lib)
-                 [org.slf4j/slf4j-log4j12 "1.7.22"]                   ; abstraction for logging frameworks -- allows end user to plug in desired logging framework at deployment time
-                 [org.yaml/snakeyaml "1.17"]                          ; YAML parser (required by liquibase)
-                 [org.xerial/sqlite-jdbc "3.8.11.2"]                  ; SQLite driver !!! DO NOT UPGRADE THIS UNTIL UPSTREAM BUG IS FIXED -- SEE https://github.com/metabase/metabase/issues/3753 !!!
+                 [org.slf4j/slf4j-log4j12 "1.7.25"]                   ; abstraction for logging frameworks -- allows end user to plug in desired logging framework at deployment time
+                 [org.yaml/snakeyaml "1.18"]                          ; YAML parser (required by liquibase)
+                 [org.xerial/sqlite-jdbc "3.16.1"]                    ; SQLite driver
                  [postgresql "9.3-1102.jdbc41"]                       ; Postgres driver
                  [io.crate/crate-jdbc "2.1.6"]                        ; Crate JDBC driver
-                 [prismatic/schema "1.1.3"]                           ; Data schema declaration and validation library
+                 [prismatic/schema "1.1.5"]                           ; Data schema declaration and validation library
                  [ring/ring-jetty-adapter "1.5.1"]                    ; Ring adapter using Jetty webserver (used to run a Ring server for unit tests)
                  [ring/ring-json "0.4.0"]                             ; Ring middleware for reading/writing JSON automatically
                  [stencil "0.5.0"]                                    ; Mustache templates for Clojure
                  [toucan "1.0.2"                                      ; Model layer, hydration, and DB utilities
                   :exclusions [honeysql]]]
   :repositories [["bintray" "https://dl.bintray.com/crate/crate"]]    ; Repo for Crate JDBC driver
-  :plugins [[lein-environ "1.0.3"]                                    ; easy access to environment variables
-            [lein-ring "0.9.7"                                        ; start the HTTP server with 'lein ring server'
+  :plugins [[lein-environ "1.1.0"]                                    ; easy access to environment variables
+            [lein-ring "0.11.0"                                       ; start the HTTP server with 'lein ring server'
              :exclusions [org.clojure/clojure]]]                      ; TODO - should this be a dev dependency ?
   :main ^:skip-aot metabase.core
   :manifest {"Liquibase-Package" "liquibase.change,liquibase.changelog,liquibase.database,liquibase.parser,liquibase.precondition,liquibase.datatype,liquibase.serializer,liquibase.sqlgenerator,liquibase.executor,liquibase.snapshot,liquibase.logging,liquibase.diff,liquibase.structure,liquibase.structurecompare,liquibase.lockservice,liquibase.sdk,liquibase.ext"}
@@ -106,13 +107,12 @@
   :docstring-checker {:include [#"^metabase"]
                       :exclude [#"test"
                                 #"^metabase\.http-client$"]}
-  :profiles {:dev {:dependencies [[org.clojure/tools.nrepl "0.2.12"]  ; REPL <3
-                                  [expectations "2.1.9"]              ; unit tests
+  :profiles {:dev {:dependencies [[expectations "2.1.9"]              ; unit tests
                                   [ring/ring-mock "0.3.0"]]           ; Library to create mock Ring requests for unit tests
                    :plugins [[docstring-checker "1.0.0"]              ; Check that all public vars have docstrings. Run with 'lein docstring-checker'
                              [jonase/eastwood "0.2.3"
                               :exclusions [org.clojure/clojure]]      ; Linting
-                             [lein-bikeshed "0.3.0"]                  ; Linting
+                             [lein-bikeshed "0.4.1"]                  ; Linting
                              [lein-expectations "0.0.8"]              ; run unit tests with 'lein expectations'
                              [lein-instant-cheatsheet "2.2.1"         ; use awesome instant cheatsheet created by yours truly w/ 'lein instant-cheatsheet'
                               :exclusions [org.clojure/clojure
diff --git a/reset_password/metabase/reset_password/core.clj b/reset_password/metabase/reset_password/core.clj
index f963a49e8dc88550ed3788842ea355119e7d5a75..510adf2a331102750cd1a5a8ccc3f54782d3249a 100644
--- a/reset_password/metabase/reset_password/core.clj
+++ b/reset_password/metabase/reset_password/core.clj
@@ -14,10 +14,10 @@
 (defn -main
   [email-address]
   (mdb/setup-db!)
-  (println (format "Resetting password for %s..." email-address))
+  (printf "Resetting password for %s...\n" email-address)
   (try
-    (println (format "OK [[[%s]]]" (set-reset-token! email-address)))
+    (printf "OK [[[%s]]]\n" (set-reset-token! email-address))
     (System/exit 0)
     (catch Throwable e
-      (println (format "FAIL [[[%s]]]" (.getMessage e)))
+      (printf "FAIL [[[%s]]]\n" (.getMessage e))
       (System/exit -1))))
diff --git a/resources/frontend_client/index_template.html b/resources/frontend_client/index_template.html
index 029a063732ed56f1032b024e1258f30871a12b25..5086fc555cda70e69ef449c369df68184726e109 100644
--- a/resources/frontend_client/index_template.html
+++ b/resources/frontend_client/index_template.html
@@ -33,7 +33,7 @@
              document.body.appendChild(script);
          }
          loadScript('https://ajax.googleapis.com/ajax/libs/webfont/1.4.7/webfont.js', function () {
-             WebFont.load({ google: { families: ["Lato:n3,n4,n7"] } });
+             WebFont.load({ google: { families: ["Lato:n3,n4,n9"] } });
          });
 
          var googleAuthClientID = window.MetabaseBootstrap.google_auth_client_id;
diff --git a/resources/migrations/052_add_query_cache_table.yaml b/resources/migrations/052_add_query_cache_table.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..e21e19f5047f4a544217a8faf4d822d624cae3b0
--- /dev/null
+++ b/resources/migrations/052_add_query_cache_table.yaml
@@ -0,0 +1,49 @@
+databaseChangeLog:
+  - property:
+        name: blob.type
+        value: blob
+        dbms: mysql,h2
+  - property:
+      name: blob.type
+      value: bytea
+      dbms: postgresql
+  - changeSet:
+      id: 52
+      author: camsaul
+      changes:
+        - createTable:
+            tableName: query_cache
+            remarks: 'Cached results of queries are stored here when using the DB-based query cache.'
+            columns:
+              - column:
+                  name: query_hash
+                  type: binary(32)
+                  remarks: 'The hash of the query dictionary. (This is a 256-bit SHA3 hash of the query dict).'
+                  constraints:
+                    primaryKey: true
+                    nullable: false
+              - column:
+                  name: updated_at
+                  type: datetime
+                  remarks: 'The timestamp of when these query results were last refreshed.'
+                  constraints:
+                    nullable: false
+              - column:
+                  name: results
+                  type: ${blob.type}
+                  remarks: 'Cached, compressed results of running the query with the given hash.'
+                  constraints:
+                    nullable: false
+        - createIndex:
+            tableName: query_cache
+            indexName: idx_query_cache_updated_at
+            columns:
+              column:
+                name: updated_at
+        - addColumn:
+            tableName: report_card
+            columns:
+              - column:
+                  name: cache_ttl
+                  type: int
+                  remarks: 'The maximum time, in seconds, to return cached results for this Card rather than running a new query.'
diff --git a/resources/migrations/053_add_query_table.yaml b/resources/migrations/053_add_query_table.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..772de999cb50a8a43200f4f043ff3034ed93d425
--- /dev/null
+++ b/resources/migrations/053_add_query_table.yaml
@@ -0,0 +1,22 @@
+databaseChangeLog:
+  - changeSet:
+      id: 53
+      author: camsaul
+      changes:
+        - createTable:
+            tableName: query
+            remarks: 'Information (such as average execution time) for different queries that have been previously ran.'
+            columns:
+              - column:
+                  name: query_hash
+                  type: binary(32)
+                  remarks: 'The hash of the query dictionary. (This is a 256-bit SHA3 hash of the query dict.)'
+                  constraints:
+                    primaryKey: true
+                    nullable: false
+              - column:
+                  name: average_execution_time
+                  type: int
+                  remarks: 'Average execution time for the query, round to nearest number of milliseconds. This is updated as a rolling average.'
+                  constraints:
+                    nullable: false
diff --git a/resources/migrations/054_add_pulse_skip_if_empty.yaml b/resources/migrations/054_add_pulse_skip_if_empty.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..054cf11db5e464d73a2e5e815b27debf943896dc
--- /dev/null
+++ b/resources/migrations/054_add_pulse_skip_if_empty.yaml
@@ -0,0 +1,15 @@
+databaseChangeLog:
+  - changeSet:
+      id: 54
+      author: tlrobinson
+      changes:
+        - addColumn:
+            tableName: pulse
+            remarks: 'Skip a scheduled Pulse if none of its questions have any results'
+            columns:
+              - column:
+                  name: skip_if_empty
+                  type: boolean
+                  defaultValueBoolean: false
+                  constraints:
+                    nullable: false
diff --git a/sample_dataset/metabase/sample_dataset/generate.clj b/sample_dataset/metabase/sample_dataset/generate.clj
index 3807dbcc1427aa73d7334a964379adae8c0a4a86..50bd431ce21ea4db945264236063c992fae29c91 100644
--- a/sample_dataset/metabase/sample_dataset/generate.clj
+++ b/sample_dataset/metabase/sample_dataset/generate.clj
@@ -253,7 +253,7 @@
           (= (count (:products %)) products)
           (every? keyword? (keys %))
           (every? sequential? (vals %))]}
-  (println (format "Generating random data: %d people, %d products..." people products))
+  (printf "Generating random data: %d people, %d products...\n" people products)
   (let [products (mapv product-add-reviews (create-randoms products random-product))
         people   (mapv (partial person-add-orders products) (create-randoms people random-person))]
     {:people   (mapv #(dissoc % :orders) people)
@@ -416,6 +416,6 @@
 
 (defn -main [& [filename]]
   (let [filename (or filename sample-dataset-filename)]
-    (println (format "Writing sample dataset to %s..." filename))
+    (printf "Writing sample dataset to %s...\n" filename)
     (create-h2-db filename)
     (System/exit 0)))
diff --git a/src/metabase/api/card.clj b/src/metabase/api/card.clj
index 3c3b09c511463c7a0e5d6d112de9f85b850a723f..0abd7ad37015666f4dc86d6e107fc87ea6d5a5eb 100644
--- a/src/metabase/api/card.clj
+++ b/src/metabase/api/card.clj
@@ -1,5 +1,6 @@
 (ns metabase.api.card
   (:require [clojure.data :as data]
+            [clojure.tools.logging :as log]
             [cheshire.core :as json]
             [compojure.core :refer [GET POST DELETE PUT]]
             [schema.core :as s]
@@ -18,10 +19,14 @@
                              [interface :as mi]
                              [label :refer [Label]]
                              [permissions :as perms]
+                             [query :as query]
                              [table :refer [Table]]
                              [view-log :refer [ViewLog]])
-            (metabase [query-processor :as qp]
-                      [util :as u])
+            [metabase.public-settings :as public-settings]
+            [metabase.query-processor :as qp]
+            [metabase.query-processor.middleware.cache :as cache]
+            [metabase.query-processor.util :as qputil]
+            [metabase.util :as u]
             [metabase.util.schema :as su])
   (:import java.util.UUID))
 
@@ -354,6 +359,26 @@
 
 ;;; ------------------------------------------------------------ Running a Query ------------------------------------------------------------
 
+(defn- query-magic-ttl
+  "Compute a 'magic' cache TTL time (in seconds) for QUERY by multipling its historic average execution times by the `query-caching-ttl-ratio`.
+   If the TTL is less than a second, this returns `nil` (i.e., the cache should not be utilized.)"
+  [query]
+  (when-let [average-duration (query/average-execution-time-ms (qputil/query-hash query))]
+    (let [ttl-seconds (Math/round (float (/ (* average-duration (public-settings/query-caching-ttl-ratio))
+                                            1000.0)))]
+      (when-not (zero? ttl-seconds)
+        (log/info (format "Question's average execution duration is %d ms; using 'magic' TTL of %d seconds" average-duration ttl-seconds) (u/emoji "💾"))
+        ttl-seconds))))
+
+(defn- query-for-card [card parameters constraints]
+  (let [query (assoc (:dataset_query card)
+                :constraints constraints
+                :parameters  parameters)
+        ttl   (when (public-settings/enable-query-caching)
+                (or (:cache_ttl card)
+                    (query-magic-ttl query)))]
+    (assoc query :cache_ttl ttl)))
+
 (defn run-query-for-card
   "Run the query for Card with PARAMETERS and CONSTRAINTS, and return results in the usual format."
   {:style/indent 1}
@@ -362,9 +387,7 @@
                      context     :question}}]
   {:pre [(u/maybe? sequential? parameters)]}
   (let [card    (read-check Card card-id)
-        query   (assoc (:dataset_query card)
-                  :parameters  parameters
-                  :constraints constraints)
+        query   (query-for-card card parameters constraints)
         options {:executed-by  *current-user-id*
                  :context      context
                  :card-id      card-id
@@ -374,26 +397,30 @@
 
 (defendpoint POST "/:card-id/query"
   "Run the query associated with a Card."
-  [card-id, :as {{:keys [parameters]} :body}]
-  (run-query-for-card card-id, :parameters parameters))
+  [card-id :as {{:keys [parameters ignore_cache], :or {ignore_cache false}} :body}]
+  {ignore_cache (s/maybe s/Bool)}
+  (binding [cache/*ignore-cached-results* ignore_cache]
+    (run-query-for-card card-id, :parameters parameters)))
 
 (defendpoint POST "/:card-id/query/csv"
   "Run the query associated with a Card, and return its results as CSV. Note that this expects the parameters as serialized JSON in the 'parameters' parameter"
   [card-id parameters]
   {parameters (s/maybe su/JSONString)}
-  (dataset-api/as-csv (run-query-for-card card-id
-                        :parameters  (json/parse-string parameters keyword)
-                        :constraints nil
-                        :context     :csv-download)))
+  (binding [cache/*ignore-cached-results* true]
+    (dataset-api/as-csv (run-query-for-card card-id
+                          :parameters  (json/parse-string parameters keyword)
+                          :constraints nil
+                          :context     :csv-download))))
 
 (defendpoint POST "/:card-id/query/json"
   "Run the query associated with a Card, and return its results as JSON. Note that this expects the parameters as serialized JSON in the 'parameters' parameter"
   [card-id parameters]
   {parameters (s/maybe su/JSONString)}
-  (dataset-api/as-json (run-query-for-card card-id
-                         :parameters  (json/parse-string parameters keyword)
-                         :constraints nil
-                         :context     :json-download)))
+  (binding [cache/*ignore-cached-results* true]
+    (dataset-api/as-json (run-query-for-card card-id
+                           :parameters  (json/parse-string parameters keyword)
+                           :constraints nil
+                           :context     :json-download))))
 
 
 ;;; ------------------------------------------------------------ Sharing is Caring ------------------------------------------------------------
diff --git a/src/metabase/api/database.clj b/src/metabase/api/database.clj
index 975dbd0f82bdf08765592bf1f7f2e2034859fe1f..c83433a7dfe6f79ab00de214f528e7c6c9fba0d1 100644
--- a/src/metabase/api/database.clj
+++ b/src/metabase/api/database.clj
@@ -143,14 +143,16 @@
   "Get a list of all `Fields` in `Database`."
   [id]
   (read-check Database id)
-  (for [{:keys [id display_name table]} (filter mi/can-read? (-> (db/select [Field :id :display_name :table_id]
-                                                                       :table_id        [:in (db/select-field :id Table, :db_id id)]
-                                                                       :visibility_type [:not-in ["sensitive" "retired"]])
-                                                                     (hydrate :table)))]
-    {:id         id
-     :name       display_name
-     :table_name (:display_name table)
-     :schema     (:schema table)}))
+  (for [{:keys [id display_name table base_type special_type]} (filter mi/can-read? (-> (db/select [Field :id :display_name :table_id :base_type :special_type]
+                                                                                                   :table_id        [:in (db/select-field :id Table, :db_id id)]
+                                                                                                   :visibility_type [:not-in ["sensitive" "retired"]])
+                                                                                        (hydrate :table)))]
+    {:id           id
+     :name         display_name
+     :base_type    base_type
+     :special_type special_type
+     :table_name   (:display_name table)
+     :schema       (:schema table)}))
 
 
 ;;; ------------------------------------------------------------ GET /api/database/:id/idfields ------------------------------------------------------------
diff --git a/src/metabase/api/dataset.clj b/src/metabase/api/dataset.clj
index ddeaf0e370cf7040ede72a001eb480c11d3e61ca..0dae851c67f8b593f82f55715825106e9029bb05 100644
--- a/src/metabase/api/dataset.clj
+++ b/src/metabase/api/dataset.clj
@@ -8,6 +8,7 @@
                     [hydrate :refer [hydrate]])
             (metabase.models [card :refer [Card]]
                              [database :refer [Database]]
+                             [query :as query]
                              [query-execution :refer [QueryExecution]])
             [metabase.query-processor :as qp]
             [metabase.query-processor.util :as qputil]
@@ -41,8 +42,8 @@
   (read-check Database database)
   ;; try calculating the average for the query as it was given to us, otherwise with the default constraints if there's no data there.
   ;; if we still can't find relevant info, just default to 0
-  {:average (or (qputil/query-average-duration query)
-                (qputil/query-average-duration (assoc query :constraints default-query-constraints))
+  {:average (or (query/average-execution-time-ms (qputil/query-hash query))
+                (query/average-execution-time-ms (qputil/query-hash (assoc query :constraints default-query-constraints)))
                 0)})
 
 (defn as-csv
diff --git a/src/metabase/api/embed.clj b/src/metabase/api/embed.clj
index a70a9eb53a0afd3f54aca3d464ce2fc1245d0219..5f713e6212dad3fbfb7260e96c61e5bb5276cfcb 100644
--- a/src/metabase/api/embed.clj
+++ b/src/metabase/api/embed.clj
@@ -125,12 +125,14 @@
   "Transforms native query's `template_tags` into `parameters`."
   [card]
   ;; NOTE: this should mirror `getTemplateTagParameters` in frontend/src/metabase/meta/Parameter.js
-  (for [[_ {tag-type :type, :as tag}] (get-in card [:dataset_query :native :template_tags])
+  (for [[_ {tag-type :type, widget-type :widget_type, :as tag}] (get-in card [:dataset_query :native :template_tags])
         :when                         (and tag-type
-                                           (not= tag-type "dimension"))]
+                                           (or widget-type (not= tag-type "dimension")))]
     {:id      (:id tag)
-     :type    (if (= tag-type "date") "date/single" "category")
-     :target  ["variable" ["template-tag" (:name tag)]]
+     :type    (or widget-type (if (= tag-type "date") "date/single" "category"))
+     :target  (if (= tag-type "dimension")
+                ["dimension" ["template-tag" (:name tag)]]
+                ["variable" ["template-tag" (:name tag)]])
      :name    (:display_name tag)
      :slug    (:name tag)
      :default (:default tag)}))
diff --git a/src/metabase/api/field.clj b/src/metabase/api/field.clj
index ecb202d74a8a988ba26610221ae2a53ff25b8e4b..4729dd7fb1d2e4ad1d065847eabd60308c45ee56 100644
--- a/src/metabase/api/field.clj
+++ b/src/metabase/api/field.clj
@@ -78,6 +78,7 @@
       (create-field-values-if-needed! field))))
 
 
+;; TODO - not sure this is used anymore
 (defendpoint POST "/:id/value_map_update"
   "Update the human-readable values for a `Field` whose special type is `category`/`city`/`state`/`country`
    or whose base type is `type/Boolean`."
diff --git a/src/metabase/api/metric.clj b/src/metabase/api/metric.clj
index cc52980b363a4afedfdc4fe818fbc57db592bac2..fcee3655552f5755b072f3df84fd00512e611d4c 100644
--- a/src/metabase/api/metric.clj
+++ b/src/metabase/api/metric.clj
@@ -31,12 +31,20 @@
   (check-superuser)
   (read-check (metric/retrieve-metric id)))
 
+(defn- add-db-ids
+  "Add `:database_id` fields to METRICS by looking them up from their `:table_id`."
+  [metrics]
+  (when (seq metrics)
+    (let [table-id->db-id (db/select-id->field :db_id Table, :id [:in (set (map :table_id metrics))])]
+      (for [metric metrics]
+        (assoc metric :database_id (table-id->db-id (:table_id metric)))))))
 
 (defendpoint GET "/"
   "Fetch *all* `Metrics`."
   [id]
   (filter mi/can-read? (-> (db/select Metric, :is_active true, {:order-by [:%lower.name]})
-                           (hydrate :creator))))
+                           (hydrate :creator)
+                           add-db-ids)))
 
 
 (defendpoint PUT "/:id"
diff --git a/src/metabase/api/pulse.clj b/src/metabase/api/pulse.clj
index 14434e35ed49f2d4ba9db1b805d59c6d3d359a65..df9bf545f23dcfb09ec7a0c5e928170e42ed9e81 100644
--- a/src/metabase/api/pulse.clj
+++ b/src/metabase/api/pulse.clj
@@ -2,6 +2,7 @@
   "/api/pulse endpoints."
   (:require [compojure.core :refer [defroutes GET PUT POST DELETE]]
             [hiccup.core :refer [html]]
+            [schema.core :as s]
             [metabase.api.common :refer :all]
             [toucan.db :as db]
             [metabase.email :as email]
@@ -38,12 +39,13 @@
 
 (defendpoint POST "/"
   "Create a new `Pulse`."
-  [:as {{:keys [name cards channels]} :body}]
-  {name     su/NonBlankString
-   cards    (su/non-empty [su/Map])
-   channels (su/non-empty [su/Map])}
+  [:as {{:keys [name cards channels skip_if_empty]} :body}]
+  {name          su/NonBlankString
+   cards         (su/non-empty [su/Map])
+   channels      (su/non-empty [su/Map])
+   skip_if_empty s/Bool}
   (check-card-read-permissions cards)
-  (check-500 (pulse/create-pulse! name *current-user-id* (map u/get-id cards) channels)))
+  (check-500 (pulse/create-pulse! name *current-user-id* (map u/get-id cards) channels skip_if_empty)))
 
 
 (defendpoint GET "/:id"
@@ -55,16 +57,18 @@
 
 (defendpoint PUT "/:id"
   "Update a `Pulse` with ID."
-  [id :as {{:keys [name cards channels]} :body}]
-  {name     su/NonBlankString
-   cards    (su/non-empty [su/Map])
-   channels (su/non-empty [su/Map])}
+  [id :as {{:keys [name cards channels skip_if_empty]} :body}]
+  {name          su/NonBlankString
+   cards         (su/non-empty [su/Map])
+   channels      (su/non-empty [su/Map])
+   skip_if_empty s/Bool}
   (write-check Pulse id)
   (check-card-read-permissions cards)
-  (pulse/update-pulse! {:id       id
-                        :name     name
-                        :cards    (map u/get-id cards)
-                        :channels channels})
+  (pulse/update-pulse! {:id             id
+                        :name           name
+                        :cards          (map u/get-id cards)
+                        :channels       channels
+                        :skip-if-empty? skip_if_empty})
   (pulse/retrieve-pulse id))
 
 
@@ -130,10 +134,11 @@
 
 (defendpoint POST "/test"
   "Test send an unsaved pulse."
-  [:as {{:keys [name cards channels] :as body} :body}]
-  {name     su/NonBlankString
-   cards    (su/non-empty [su/Map])
-   channels (su/non-empty [su/Map])}
+  [:as {{:keys [name cards channels skip_if_empty] :as body} :body}]
+  {name          su/NonBlankString
+   cards         (su/non-empty [su/Map])
+   channels      (su/non-empty [su/Map])
+   skip_if_empty s/Bool}
   (check-card-read-permissions cards)
   (p/send-pulse! body)
   {:ok true})
diff --git a/src/metabase/api/routes.clj b/src/metabase/api/routes.clj
index 0d2da6f8c2c675a2e1b97a138cbbe942de133a88..20ed2d16ff23ed87ca3a8d8bc7fae9f68015b08f 100644
--- a/src/metabase/api/routes.clj
+++ b/src/metabase/api/routes.clj
@@ -60,9 +60,6 @@
   (context "/field"           [] (+auth field/routes))
   (context "/getting_started" [] (+auth getting-started/routes))
   (context "/geojson"         [] (+auth geojson/routes))
-  (GET     "/health"          [] (if ((resolve 'metabase.core/initialized?))
-                                   {:status 200, :body {:status "ok"}}
-                                   {:status 503, :body {:status "initializing", :progress ((resolve 'metabase.core/initialization-progress))}}))
   (context "/label"           [] (+auth label/routes))
   (context "/metric"          [] (+auth metric/routes))
   (context "/notify"          [] (+apikey notify/routes))
diff --git a/src/metabase/cmd/load_from_h2.clj b/src/metabase/cmd/load_from_h2.clj
index cd73db016935c026d12edb1c7664b014656df981..879b4e35ac043a33068fcadc2387b8b9c38370ec 100644
--- a/src/metabase/cmd/load_from_h2.clj
+++ b/src/metabase/cmd/load_from_h2.clj
@@ -46,7 +46,6 @@
                              [pulse-card :refer [PulseCard]]
                              [pulse-channel :refer [PulseChannel]]
                              [pulse-channel-recipient :refer [PulseChannelRecipient]]
-                             [query-execution :refer [QueryExecution]]
                              [raw-column :refer [RawColumn]]
                              [raw-table :refer [RawTable]]
                              [revision :refer [Revision]]
@@ -89,7 +88,6 @@
    DashboardCard
    DashboardCardSeries
    Activity
-   QueryExecution
    Pulse
    PulseCard
    PulseChannel
diff --git a/src/metabase/config.clj b/src/metabase/config.clj
index e5d45b1b6c7dab6b361c24b6554237862ea7bc29..ac7ff3fd221372021a4a8776af0136b0b9ad5fa1 100644
--- a/src/metabase/config.clj
+++ b/src/metabase/config.clj
@@ -29,7 +29,8 @@
    :mb-version-info-url "http://static.metabase.com/version-info.json"
    :max-session-age "20160"                     ; session length in minutes (14 days)
    :mb-colorize-logs "true"
-   :mb-emoji-in-logs "true"})
+   :mb-emoji-in-logs "true"
+   :mb-qp-cache-backend "db"})
 
 
 (defn config-str
diff --git a/src/metabase/core.clj b/src/metabase/core.clj
index 2b8a82c194b8bba0bb7c4735b10aa9851b1b67cb..571ae867587aa8bcbe03f53e6a779b01e91091f1 100644
--- a/src/metabase/core.clj
+++ b/src/metabase/core.clj
@@ -1,7 +1,8 @@
 ;; -*- comment-column: 35; -*-
 (ns metabase.core
   (:gen-class)
-  (:require [clojure.string :as s]
+  (:require (clojure [pprint :as pprint]
+                     [string :as s])
             [clojure.tools.logging :as log]
             environ.core
             [ring.adapter.jetty :as ring-jetty]
@@ -14,8 +15,9 @@
                              [session :refer [wrap-session]])
             [medley.core :as m]
             [toucan.db :as db]
-            (metabase [config :as config]
-                      [db :as mdb]
+            [metabase.config :as config]
+            [metabase.core.initialization-status :as init-status]
+            (metabase [db :as mdb]
                       [driver :as driver]
                       [events :as events]
                       [logger :as logger]
@@ -54,23 +56,7 @@
 
 ;;; ## ---------------------------------------- LIFECYCLE ----------------------------------------
 
-(defonce ^:private metabase-initialization-progress
-  (atom 0))
 
-(defn initialized?
-  "Is Metabase initialized and ready to be served?"
-  []
-  (= @metabase-initialization-progress 1.0))
-
-(defn initialization-progress
-  "Get the current progress of Metabase initialization."
-  []
-  @metabase-initialization-progress)
-
-(defn initialization-complete!
-  "Complete the Metabase initialization by setting its progress to 100%."
-  []
-  (reset! metabase-initialization-progress 1.0))
 
 (defn- -init-create-setup-token
   "Create and set a new setup token and log it."
@@ -97,46 +83,47 @@
   []
   (log/info (format "Starting Metabase version %s ..." config/mb-version-string))
   (log/info (format "System timezone is '%s' ..." (System/getProperty "user.timezone")))
-  (reset! metabase-initialization-progress 0.1)
+  (init-status/set-progress! 0.1)
 
   ;; First of all, lets register a shutdown hook that will tidy things up for us on app exit
   (.addShutdownHook (Runtime/getRuntime) (Thread. ^Runnable destroy!))
-  (reset! metabase-initialization-progress 0.2)
+  (init-status/set-progress! 0.2)
 
   ;; load any plugins as needed
   (plugins/load-plugins!)
-  (reset! metabase-initialization-progress 0.3)
+  (init-status/set-progress! 0.3)
 
   ;; Load up all of our Database drivers, which are used for app db work
   (driver/find-and-load-drivers!)
-  (reset! metabase-initialization-progress 0.4)
+  (init-status/set-progress! 0.4)
 
   ;; startup database.  validates connection & runs any necessary migrations
+  (log/info "Setting up and migrating Metabase DB. Please sit tight, this may take a minute...")
   (mdb/setup-db! :auto-migrate (config/config-bool :mb-db-automigrate))
-  (reset! metabase-initialization-progress 0.5)
+  (init-status/set-progress! 0.5)
 
   ;; run a very quick check to see if we are doing a first time installation
   ;; the test we are using is if there is at least 1 User in the database
-  (let [new-install (not (db/exists? User))]
+  (let [new-install? (not (db/exists? User))]
 
     ;; Bootstrap the event system
     (events/initialize-events!)
-    (reset! metabase-initialization-progress 0.7)
+    (init-status/set-progress! 0.7)
 
     ;; Now start the task runner
     (task/start-scheduler!)
-    (reset! metabase-initialization-progress 0.8)
+    (init-status/set-progress! 0.8)
 
-    (when new-install
+    (when new-install?
       (log/info "Looks like this is a new installation ... preparing setup wizard")
       ;; create setup token
       (-init-create-setup-token)
       ;; publish install event
       (events/publish-event! :install {}))
-    (reset! metabase-initialization-progress 0.9)
+    (init-status/set-progress! 0.9)
 
     ;; deal with our sample dataset as needed
-    (if new-install
+    (if new-install?
       ;; add the sample dataset DB for fresh installs
       (sample-data/add-sample-dataset!)
       ;; otherwise update if appropriate
@@ -145,7 +132,7 @@
     ;; start the metabot thread
     (metabot/start-metabot!))
 
-  (initialization-complete!)
+  (init-status/set-complete!)
   (log/info "Metabase Initialization COMPLETE"))
 
 
@@ -173,7 +160,8 @@
                              (config/config-str :mb-jetty-daemon) (assoc :daemon? (config/config-bool :mb-jetty-daemon))
                              (config/config-str :mb-jetty-ssl)    (-> (assoc :ssl? true)
                                                                       (merge jetty-ssl-config)))]
-      (log/info "Launching Embedded Jetty Webserver with config:\n" (with-out-str (clojure.pprint/pprint (m/filter-keys (fn [k] (not (re-matches #".*password.*" (str k)))) jetty-config))))
+      (log/info "Launching Embedded Jetty Webserver with config:\n" (with-out-str (pprint/pprint (m/filter-keys #(not (re-matches #".*password.*" (str %)))
+                                                                                                                jetty-config))))
       ;; NOTE: we always start jetty w/ join=false so we can start the server first then do init in the background
       (->> (ring-jetty/run-jetty app (assoc jetty-config :join? false))
            (reset! jetty-instance)))))
diff --git a/src/metabase/core/initialization_status.clj b/src/metabase/core/initialization_status.clj
new file mode 100644
index 0000000000000000000000000000000000000000..953d6040f0b9d6f8cf1d0809931bedf1ac268ebe
--- /dev/null
+++ b/src/metabase/core/initialization_status.clj
@@ -0,0 +1,28 @@
+(ns metabase.core.initialization-status
+  "Code related to tracking the progress of metabase initialization.
+   This is kept in a separate, tiny namespace so it can be loaded right away when the application launches
+   (and so we don't need to wait for `metabase.core` to load to check the status).")
+
+(defonce ^:private progress-atom
+  (atom 0))
+
+(defn complete?
+  "Is Metabase initialized and ready to be served?"
+  []
+  (= @progress-atom 1.0))
+
+(defn progress
+  "Get the current progress of Metabase initialization."
+  []
+  @progress-atom)
+
+(defn set-progress!
+  "Update the Metabase initialization progress to a new value, a floating-point value between `0` and `1`."
+  [^Float new-progress]
+  {:pre [(float? new-progress) (<= 0.0 new-progress 1.0)]}
+  (reset! progress-atom new-progress))
+
+(defn set-complete!
+  "Complete the Metabase initialization by setting its progress to 100%."
+  []
+  (set-progress! 1.0))
diff --git a/src/metabase/db.clj b/src/metabase/db.clj
index 044bb3f922d2009129bae9deb4a715fac71dee21..75fcf7b19b7ff3c9325796c3ab3867bdff9c81a3 100644
--- a/src/metabase/db.clj
+++ b/src/metabase/db.clj
@@ -19,6 +19,7 @@
             metabase.util.honeysql-extensions) ; this needs to be loaded so the `:h2` quoting style gets added
   (:import java.io.StringWriter
            java.sql.Connection
+           java.util.Properties
            com.mchange.v2.c3p0.ComboPooledDataSource
            liquibase.Liquibase
            (liquibase.database DatabaseFactory Database)
@@ -268,7 +269,7 @@
                  (.setTestConnectionOnCheckin      false)
                  (.setTestConnectionOnCheckout     false)
                  (.setPreferredTestQuery           nil)
-                 (.setProperties                   (u/prog1 (java.util.Properties.)
+                 (.setProperties                   (u/prog1 (Properties.)
                                                      (doseq [[k v] (dissoc spec :classname :subprotocol :subname :naming :delimiters :alias-delimiter
                                                                                 :excess-timeout :minimum-pool-size :idle-connection-test-period)]
                                                        (.setProperty <> (name k) (str v))))))})
@@ -289,6 +290,11 @@
 (def ^:private setup-db-has-been-called?
   (atom false))
 
+(defn db-is-setup?
+  "True if the Metabase DB is setup and ready."
+  ^Boolean []
+  @setup-db-has-been-called?)
+
 (def ^:dynamic *allow-potentailly-unsafe-connections*
   "We want to make *every* database connection made by the drivers safe -- read-only, only connect if DB file exists, etc.
    At the same time, we'd like to be able to use driver functionality like `can-connect-with-details?` to check whether we can
@@ -360,11 +366,11 @@
   [& {:keys [db-details auto-migrate]
       :or   {db-details   @db-connection-details
              auto-migrate true}}]
-  (reset! setup-db-has-been-called? true)
   (verify-db-connection db-details)
   (run-schema-migrations! auto-migrate db-details)
   (create-connection-pool! (jdbc-details db-details))
-  (run-data-migrations!))
+  (run-data-migrations!)
+  (reset! setup-db-has-been-called? true))
 
 (defn setup-db-if-needed!
   "Call `setup-db!` if DB is not already setup; otherwise this does nothing."
diff --git a/src/metabase/db/spec.clj b/src/metabase/db/spec.clj
index 969dfbd97f9f49dc1311f292d17c00bc94bca115..3b7454c891e447494a631bfe51742e9f8cb3720c 100644
--- a/src/metabase/db/spec.clj
+++ b/src/metabase/db/spec.clj
@@ -1,5 +1,7 @@
 (ns metabase.db.spec
-  "Functions for creating JDBC DB specs for a given engine.")
+  "Functions for creating JDBC DB specs for a given engine.
+   Only databases that are supported as application DBs should have functions in this namespace;
+   otherwise, similar functions are only needed by drivers, and belong in those namespaces.")
 
 (defn h2
   "Create a database specification for a h2 database. Opts should include a key
@@ -36,38 +38,3 @@
           :subname (str "//" host ":" port "/" db)
           :delimiters "`"}
          (dissoc opts :host :port :db)))
-
-
-;; TODO - These other ones can acutally be moved directly into their respective drivers themselves since they're not supported as backing DBs
-
-(defn mssql
-  "Create a database specification for a mssql database. Opts should include keys
-  for :db, :user, and :password. You can also optionally set host and port."
-  [{:keys [user password db host port]
-    :or {user "dbuser", password "dbpassword", db "", host "localhost", port 1433}
-    :as opts}]
-  (merge {:classname "com.microsoft.sqlserver.jdbc.SQLServerDriver" ; must be in classpath
-          :subprotocol "sqlserver"
-          :subname (str "//" host ":" port ";database=" db ";user=" user ";password=" password)}
-         (dissoc opts :host :port :db)))
-
-(defn sqlite3
-  "Create a database specification for a SQLite3 database. Opts should include a
-  key for :db which is the path to the database file."
-  [{:keys [db]
-    :or {db "sqlite.db"}
-    :as opts}]
-  (merge {:classname "org.sqlite.JDBC" ; must be in classpath
-          :subprotocol "sqlite"
-          :subname db}
-         (dissoc opts :db)))
-
-(defn oracle
-  "Create a database specification for an Oracle database. Opts should include keys
-  for :user and :password. You can also optionally set host and port."
-  [{:keys [host port]
-    :or {host "localhost", port 1521}
-    :as opts}]
-  (merge {:subprotocol "oracle:thin"
-          :subname     (str "@" host ":" port)}
-         (dissoc opts :host :port)))
diff --git a/src/metabase/driver/bigquery.clj b/src/metabase/driver/bigquery.clj
index 8e4c41e194baa3c014da31a0f84141ef7332734b..e979b235e799f2c45ea0ab408e1f65d60c472353 100644
--- a/src/metabase/driver/bigquery.clj
+++ b/src/metabase/driver/bigquery.clj
@@ -382,6 +382,12 @@
 (defn- string-length-fn [field-key]
   (hsql/call :length field-key))
 
+;; From the dox: Fields must contain only letters, numbers, and underscores, start with a letter or underscore, and be at most 128 characters long.
+(defn- format-custom-field-name ^String [^String custom-field-name]
+  (s/join (take 128 (-> (s/trim custom-field-name)
+                        (s/replace #"[^\w\d_]" "_")
+                        (s/replace #"(^\d)" "_$1")))))
+
 
 (defrecord BigQueryDriver []
   clojure.lang.Named
@@ -407,46 +413,45 @@
 
   driver/IDriver
   (merge driver/IDriverDefaultsMixin
-         {:analyze-table         analyze/generic-analyze-table
-          :can-connect?          (u/drop-first-arg can-connect?)
-          :date-interval         (u/drop-first-arg (comp prepare-value u/relative-date))
-          :describe-database     (u/drop-first-arg describe-database)
-          :describe-table        (u/drop-first-arg describe-table)
-          :details-fields        (constantly [{:name         "project-id"
-                                               :display-name "Project ID"
-                                               :placeholder  "praxis-beacon-120871"
-                                               :required     true}
-                                              {:name         "dataset-id"
-                                               :display-name "Dataset ID"
-                                               :placeholder  "toucanSightings"
-                                               :required     true}
-                                              {:name         "client-id"
-                                               :display-name "Client ID"
-                                               :placeholder  "1201327674725-y6ferb0feo1hfssr7t40o4aikqll46d4.apps.googleusercontent.com"
-                                               :required     true}
-                                              {:name         "client-secret"
-                                               :display-name "Client Secret"
-                                               :placeholder  "dJNi4utWgMzyIFo2JbnsK6Np"
-                                               :required     true}
-                                              {:name         "auth-code"
-                                               :display-name "Auth Code"
-                                               :placeholder  "4/HSk-KtxkSzTt61j5zcbee2Rmm5JHkRFbL5gD5lgkXek"
-                                               :required     true}])
-          :execute-query         (u/drop-first-arg execute-query)
+         {:analyze-table            analyze/generic-analyze-table
+          :can-connect?             (u/drop-first-arg can-connect?)
+          :date-interval            (u/drop-first-arg (comp prepare-value u/relative-date))
+          :describe-database        (u/drop-first-arg describe-database)
+          :describe-table           (u/drop-first-arg describe-table)
+          :details-fields           (constantly [{:name         "project-id"
+                                                  :display-name "Project ID"
+                                                  :placeholder  "praxis-beacon-120871"
+                                                  :required     true}
+                                                 {:name         "dataset-id"
+                                                  :display-name "Dataset ID"
+                                                  :placeholder  "toucanSightings"
+                                                  :required     true}
+                                                 {:name         "client-id"
+                                                  :display-name "Client ID"
+                                                  :placeholder  "1201327674725-y6ferb0feo1hfssr7t40o4aikqll46d4.apps.googleusercontent.com"
+                                                  :required     true}
+                                                 {:name         "client-secret"
+                                                  :display-name "Client Secret"
+                                                  :placeholder  "dJNi4utWgMzyIFo2JbnsK6Np"
+                                                  :required     true}
+                                                 {:name         "auth-code"
+                                                  :display-name "Auth Code"
+                                                  :placeholder  "4/HSk-KtxkSzTt61j5zcbee2Rmm5JHkRFbL5gD5lgkXek"
+                                                  :required     true}])
+          :execute-query            (u/drop-first-arg execute-query)
           ;; Don't enable foreign keys when testing because BigQuery *doesn't* have a notion of foreign keys. Joins are still allowed, which puts us in a weird position, however;
           ;; people can manually specifiy "foreign key" relationships in admin and everything should work correctly.
           ;; Since we can't infer any "FK" relationships during sync our normal FK tests are not appropriate for BigQuery, so they're disabled for the time being.
           ;; TODO - either write BigQuery-speciifc tests for FK functionality or add additional code to manually set up these FK relationships for FK tables
-          :features              (constantly (set/union #{:basic-aggregations
-                                                          :standard-deviation-aggregations
-                                                          :native-parameters
-                                                          ;; Expression aggregations *would* work, but BigQuery doesn't support the auto-generated column names. BQ column names
-                                                          ;; can only be alphanumeric or underscores. If we slugified the auto-generated column names, we could enable this feature.
-                                                          #_:expression-aggregations}
-                                                        (when-not config/is-test?
-                                                          ;; during unit tests don't treat bigquery as having FK support
-                                                          #{:foreign-keys})))
-          :field-values-lazy-seq (u/drop-first-arg field-values-lazy-seq)
-          :mbql->native          (u/drop-first-arg mbql->native)}))
+          :features                 (constantly (set/union #{:basic-aggregations
+                                                             :standard-deviation-aggregations
+                                                             :native-parameters
+                                                             :expression-aggregations}
+                                                           (when-not config/is-test?
+                                                             ;; during unit tests don't treat bigquery as having FK support
+                                                             #{:foreign-keys})))
+          :field-values-lazy-seq    (u/drop-first-arg field-values-lazy-seq)
+          :format-custom-field-name (u/drop-first-arg format-custom-field-name)
+          :mbql->native             (u/drop-first-arg mbql->native)}))
 
 (driver/register-driver! :bigquery driver)
diff --git a/src/metabase/driver/crate.clj b/src/metabase/driver/crate.clj
index 00cef3a39a50eea126019b0060b006a3059e6731..6d88591a90c6febca681fce0017ddc09e696d769 100644
--- a/src/metabase/driver/crate.clj
+++ b/src/metabase/driver/crate.clj
@@ -40,16 +40,16 @@
 
 (def ^:private ^:const now (hsql/call :current_timestamp 3))
 
-(defn- crate-spec
+(defn- connection-details->spec
   [{:keys [hosts]
-    :as   opts}]
+    :as   details}]
   (merge {:classname   "io.crate.client.jdbc.CrateDriver" ; must be in classpath
           :subprotocol "crate"
           :subname     (str "//" hosts "/")}
-         (dissoc opts :hosts)))
+         (dissoc details :hosts)))
 
 (defn- can-connect? [details]
-  (let [connection-spec (crate-spec details)]
+  (let [connection-spec (connection-details->spec details)]
     (= 1 (first (vals (first (jdbc/query connection-spec ["select 1"])))))))
 
 (defn- string-length-fn [field-key]
@@ -71,7 +71,7 @@
           :features       (comp (u/rpartial disj :foreign-keys) sql/features)})
   sql/ISQLDriver
   (merge (sql/ISQLDriverDefaultsMixin)
-         {:connection-details->spec  (u/drop-first-arg crate-spec)
+         {:connection-details->spec  (u/drop-first-arg connection-details->spec)
           :column->base-type         (u/drop-first-arg column->base-type)
           :string-length-fn          (u/drop-first-arg string-length-fn)
           :date                      crate-util/date
diff --git a/src/metabase/driver/druid.clj b/src/metabase/driver/druid.clj
index 6f652e66cf5b1a5e6bcb642b4b3c152275923c38..1cfe762537c4f60b095724370b24c44a2de2a3ef 100644
--- a/src/metabase/driver/druid.clj
+++ b/src/metabase/driver/druid.clj
@@ -7,6 +7,7 @@
             [metabase.driver.druid.query-processor :as qp]
             (metabase.models [field :as field]
                              [table :as table])
+            [metabase.sync-database.analyze :as analyze]
             [metabase.util :as u]))
 
 ;;; ### Request helper fns
@@ -138,6 +139,15 @@
                          (field-values-lazy-seq details table-name field-name total-items-fetched paging-identifiers)))))))
 
 
+(defn- analyze-table
+  "Implementation of `analyze-table` for Druid driver."
+  [driver table new-table-ids]
+  ((analyze/make-analyze-table driver
+     :field-avg-length-fn   (constantly 0) ; TODO implement this?
+     :field-percent-urls-fn (constantly 0)
+     :calculate-row-count?  false) driver table new-table-ids))
+
+
 ;;; ### DruidrDriver Class Definition
 
 (defrecord DruidDriver []
@@ -148,6 +158,7 @@
   driver/IDriver
   (merge driver/IDriverDefaultsMixin
          {:can-connect?          (u/drop-first-arg can-connect?)
+          :analyze-table         analyze-table
           :describe-database     (u/drop-first-arg describe-database)
           :describe-table        (u/drop-first-arg describe-table)
           :details-fields        (constantly [{:name         "host"
diff --git a/src/metabase/driver/druid/query_processor.clj b/src/metabase/driver/druid/query_processor.clj
index 6b260f2a07866bb7790eef27f8c21963e505e974..7ec2887b4fc640deb8b0540d77b50901a5bac52c 100644
--- a/src/metabase/driver/druid/query_processor.clj
+++ b/src/metabase/driver/druid/query_processor.clj
@@ -57,8 +57,8 @@
   Field                 (->rvalue [this] (:field-name this))
   DateTimeField         (->rvalue [this] (->rvalue (:field this)))
   Value                 (->rvalue [this] (:value this))
-  DateTimeValue         (->rvalue [{{unit :unit} :field, value :value}] (u/date->iso-8601 (u/date-trunc-or-extract unit value (get-timezone-id))))
-  RelativeDateTimeValue (->rvalue [{:keys [unit amount]}] (u/date->iso-8601 (u/date-trunc-or-extract unit (u/relative-date unit amount) (get-timezone-id)))))
+  DateTimeValue         (->rvalue [{{unit :unit} :field, value :value}] (u/date->iso-8601 (u/date-trunc unit value (get-timezone-id))))
+  RelativeDateTimeValue (->rvalue [{:keys [unit amount]}] (u/date->iso-8601 (u/date-trunc unit (u/relative-date unit amount) (get-timezone-id)))))
 
 (defprotocol ^:private IDimensionOrMetric
   (^:private dimension-or-metric? [this]
diff --git a/src/metabase/driver/generic_sql.clj b/src/metabase/driver/generic_sql.clj
index bd9610dc28500b1732fa2ec1d29ffa41fb663eed..dbec2699e8e5f9c135fab69c7843b3bfcb5edf3b 100644
--- a/src/metabase/driver/generic_sql.clj
+++ b/src/metabase/driver/generic_sql.clj
@@ -15,7 +15,7 @@
             [metabase.sync-database.analyze :as analyze]
             [metabase.util :as u]
             [metabase.util.honeysql-extensions :as hx])
-  (:import java.sql.DatabaseMetaData
+  (:import (java.sql DatabaseMetaData ResultSet)
            java.util.Map
            (clojure.lang Keyword PersistentVector)
            com.mchange.v2.c3p0.ComboPooledDataSource
@@ -27,9 +27,12 @@
    Methods marked *OPTIONAL* have default implementations in `ISQLDriverDefaultsMixin`."
 
   (active-tables ^java.util.Set [this, ^DatabaseMetaData metadata]
-    "Return a set of maps containing information about the active tables/views, collections, or equivalent that currently exist in DATABASE.
+    "*OPTIONAL* Return a set of maps containing information about the active tables/views, collections, or equivalent that currently exist in DATABASE.
      Each map should contain the key `:name`, which is the string name of the table. For databases that have a concept of schemas,
-     this map should also include the string name of the table's `:schema`.")
+     this map should also include the string name of the table's `:schema`.
+
+   Two different implementations are provided in this namespace: `fast-active-tables` (the default), and `post-filtered-active-tables`. You should be fine using
+   the default, but refer to the documentation for those functions for more details on the differences.")
 
   ;; The following apply-* methods define how the SQL Query Processor handles given query clauses. Each method is called when a matching clause is present
   ;; in QUERY, and should return an appropriately modified version of KORMA-QUERY. Most drivers can use the default implementations for all of these methods,
@@ -71,7 +74,7 @@
   (field-percent-urls [this field]
     "*OPTIONAL*. Implementation of the `:field-percent-urls-fn` to be passed to `make-analyze-table`.
      The default implementation is `fast-field-percent-urls`, which avoids a full table scan. Substitue this with `slow-field-percent-urls` for databases
-     where this doesn't work, such as SQL Server")
+     where this doesn't work, such as SQL Server.")
 
   (field->alias ^String [this, ^Field field]
     "*OPTIONAL*. Return the alias that should be used to for FIELD, i.e. in an `AS` clause. The default implementation calls `name`, which
@@ -327,7 +330,6 @@
 
 ;;; ## Database introspection methods used by sync process
 
-;; TODO - clojure.java.jdbc now ships with a `metadata-query` function we could use here. See #2918
 (defmacro with-metadata
   "Execute BODY with `java.sql.DatabaseMetaData` for DATABASE."
   [[binding _ database] & body]
@@ -335,6 +337,12 @@
      (let [~binding (.getMetaData conn#)]
        ~@body)))
 
+(defn- get-tables
+  "Fetch a JDBC Metadata ResultSet of tables in the DB, optionally limited to ones belonging to a given schema."
+  ^ResultSet [^DatabaseMetaData metadata, ^String schema-or-nil]
+  (jdbc/result-set-seq (.getTables metadata nil schema-or-nil "%" ; tablePattern "%" = match all tables
+                                   (into-array String ["TABLE", "VIEW", "FOREIGN TABLE", "MATERIALIZED VIEW"]))))
+
 (defn fast-active-tables
   "Default, fast implementation of `ISQLDriver/active-tables` best suited for DBs with lots of system tables (like Oracle).
    Fetch list of schemas, then for each one not in `excluded-schemas`, fetch its Tables, and combine the results.
@@ -344,7 +352,7 @@
   (let [all-schemas (set (map :table_schem (jdbc/result-set-seq (.getSchemas metadata))))
         schemas     (set/difference all-schemas (excluded-schemas driver))]
     (set (for [schema     schemas
-               table-name (mapv :table_name (jdbc/result-set-seq (.getTables metadata nil schema "%" (into-array String ["TABLE", "VIEW", "FOREIGN TABLE"]))))] ; tablePattern "%" = match all tables
+               table-name (mapv :table_name (get-tables metadata schema))]
            {:name   table-name
             :schema schema}))))
 
@@ -353,7 +361,7 @@
    Fetch *all* Tables, then filter out ones whose schema is in `excluded-schemas` Clojure-side."
   [driver, ^DatabaseMetaData metadata]
   (set (for [table (filter #(not (contains? (excluded-schemas driver) (:table_schem %)))
-                           (jdbc/result-set-seq (.getTables metadata nil nil "%" (into-array String ["TABLE", "VIEW", "FOREIGN TABLE"]))))] ; tablePattern "%" = match all tables
+                           (get-tables metadata nil))]
          {:name   (:table_name table)
           :schema (:table_schem table)})))
 
diff --git a/src/metabase/driver/generic_sql/query_processor.clj b/src/metabase/driver/generic_sql/query_processor.clj
index 3a19281b242c7d0705e087f545cd94d2a132088c..741a791a1a35a4ea6836755639fe631c7b6bd9da 100644
--- a/src/metabase/driver/generic_sql/query_processor.clj
+++ b/src/metabase/driver/generic_sql/query_processor.clj
@@ -151,7 +151,7 @@
 
 (defn- apply-expression-aggregation [driver honeysql-form expression]
   (h/merge-select honeysql-form [(expression-aggregation->honeysql driver expression)
-                                 (hx/escape-dots (annotate/aggregation-name expression))]))
+                                 (hx/escape-dots (driver/format-custom-field-name driver (annotate/aggregation-name expression)))]))
 
 (defn- apply-single-aggregation [driver honeysql-form {:keys [aggregation-type field], :as aggregation}]
   (h/merge-select honeysql-form [(aggregation->honeysql driver aggregation-type field)
diff --git a/src/metabase/driver/generic_sql/util/unprepare.clj b/src/metabase/driver/generic_sql/util/unprepare.clj
index d65502ac180912785228c1ac8058e590ed548f71..56845e692a68a5e0a3500406c0c5609ebed4ffcd 100644
--- a/src/metabase/driver/generic_sql/util/unprepare.clj
+++ b/src/metabase/driver/generic_sql/util/unprepare.clj
@@ -7,20 +7,20 @@
   (:import java.util.Date))
 
 (defprotocol ^:private IUnprepare
-  (^:private unprepare-arg ^String [this]))
+  (^:private unprepare-arg ^String [this settings]))
 
 (extend-protocol IUnprepare
-  nil     (unprepare-arg [this] "NULL")
-  String  (unprepare-arg [this] (str \' (str/replace this "'" "\\\\'") \')) ; escape single-quotes
-  Boolean (unprepare-arg [this] (if this "TRUE" "FALSE"))
-  Number  (unprepare-arg [this] (str this))
-  Date    (unprepare-arg [this] (first (hsql/format (hsql/call :timestamp (hx/literal (u/date->iso-8601 this))))))) ; TODO - this probably doesn't work for every DB!
+  nil     (unprepare-arg [this _] "NULL")
+  String  (unprepare-arg [this {:keys [quote-escape]}] (str \' (str/replace this "'" (str quote-escape "'")) \')) ; escape single-quotes
+  Boolean (unprepare-arg [this _] (if this "TRUE" "FALSE"))
+  Number  (unprepare-arg [this _] (str this))
+  Date    (unprepare-arg [this {:keys [iso-8601-fn]}] (first (hsql/format (hsql/call iso-8601-fn (hx/literal (u/date->iso-8601 this)))))))
 
 (defn unprepare
   "Convert a normal SQL `[statement & prepared-statement-args]` vector into a flat, non-prepared statement."
-  ^String [[sql & args]]
+  ^String [[sql & args] & {:keys [quote-escape iso-8601-fn], :or {quote-escape "\\\\", iso-8601-fn :timestamp}}]
   (loop [sql sql, [arg & more-args, :as args] args]
     (if-not (seq args)
       sql
-      (recur (str/replace-first sql #"(?<!\?)\?(?!\?)" (unprepare-arg arg))
+      (recur (str/replace-first sql #"(?<!\?)\?(?!\?)" (unprepare-arg arg {:quote-escape quote-escape, :iso-8601-fn iso-8601-fn}))
              more-args))))
diff --git a/src/metabase/driver/mongo/query_processor.clj b/src/metabase/driver/mongo/query_processor.clj
index 4e27d66d57140600f9380fc92e11de81b4643a67..4dac3f89814c8677bfd63ebbcaad925229b68545 100644
--- a/src/metabase/driver/mongo/query_processor.clj
+++ b/src/metabase/driver/mongo/query_processor.clj
@@ -379,31 +379,63 @@
                     v)}))))
 
 
-;;; ------------------------------------------------------------ Handling ISODate(...) forms ------------------------------------------------------------
-;; In Mongo it's fairly common use ISODate(...) forms in queries, which unfortunately are not valid JSON,
+;;; ------------------------------------------------------------ Handling ISODate(...) and ObjectId(...) forms ------------------------------------------------------------
+;; In Mongo it's fairly common use ISODate(...) or ObjectId(...) forms in queries, which unfortunately are not valid JSON,
 ;; and thus cannot be parsed by Cheshire. But we are clever so we will:
 ;;
 ;; 1) Convert forms like ISODate(...) to valid JSON forms like ["___ISODate", ...]
 ;; 2) Parse Normally
-;; 3) Walk the parsed JSON and convert forms like [:___ISODate ...] to JodaTime dates
+;; 3) Walk the parsed JSON and convert forms like [:___ISODate ...] to JodaTime dates, and [:___ObjectId ...] to BSON IDs
+
+;; add more fn handlers here as needed
+(def ^:private fn-name->decoder
+  {:ISODate (fn [arg]
+              (DateTime. arg))
+   :ObjectId (fn [^String arg]
+               (ObjectId. arg))})
+
+(defn- form->encoded-fn-name
+  "If FORM is an encoded fn call form return the key representing the fn call that was encoded.
+   If it doesn't represent an encoded fn, return `nil`.
+
+     (form->encoded-fn-name [:___ObjectId \"583327789137b2700a1621fb\"]) -> :ObjectId"
+  [form]
+  (when (vector? form)
+    (when (u/string-or-keyword? (first form))
+      (when-let [[_ k] (re-matches #"^___(\w+$)" (name (first form)))]
+        (let [k (keyword k)]
+          (when (contains? fn-name->decoder k)
+            k))))))
+
+(defn- maybe-decode-fncall [form]
+  (if-let [fn-name (form->encoded-fn-name form)]
+    ((fn-name->decoder fn-name) (second form))
+    form))
 
-(defn- encoded-iso-date? [form]
-  (and (vector? form)
-       (= (first form) "___ISODate")))
+(defn- decode-fncalls [query]
+  (walk/postwalk maybe-decode-fncall query))
 
-(defn- maybe-decode-iso-date-fncall [form]
-  (if (encoded-iso-date? form)
-    (DateTime. (second form))
-    form))
+(defn- encode-fncalls-for-fn
+  "Walk QUERY-STRING and replace fncalls to fn with FN-NAME with encoded forms that can be parsed as valid JSON.
+
+     (encode-fncalls-for-fn \"ObjectId\" \"{\\\"$match\\\":ObjectId(\\\"583327789137b2700a1621fb\\\")}\")
+     ;; -> \"{\\\"$match\\\":[\\\"___ObjectId\\\", \\\"583327789137b2700a1621fb\\\"]}\""
+  [fn-name query-string]
+  (s/replace query-string
+             (re-pattern (format "%s\\(([^)]+)\\)" (name fn-name)))
+             (format "[\"___%s\", $1]" (name fn-name))))
 
-(defn- decode-iso-date-fncalls [query]
-  (walk/postwalk maybe-decode-iso-date-fncall query))
+(defn- encode-fncalls
+  "Replace occurances of `ISODate(...)` and similary function calls (invalid JSON, but legal in Mongo)
+   with legal JSON forms like `[:___ISODate ...]` that we can decode later.
 
-(defn- encode-iso-date-fncalls
-  "Replace occurances of `ISODate(...)` function calls (invalid JSON, but legal in Mongo)
-   with legal JSON forms like `[:___ISODate ...]` that we can decode later."
+   Walks QUERY-STRING and encodes all the various fncalls we support."
   [query-string]
-  (s/replace query-string #"ISODate\(([^)]+)\)" "[\"___ISODate\", $1]"))
+  (loop [query-string query-string, [fn-name & more] (keys fn-name->decoder)]
+    (if-not fn-name
+      query-string
+      (recur (encode-fncalls-for-fn fn-name query-string)
+             more))))
 
 
 ;;; ------------------------------------------------------------ Query Execution ------------------------------------------------------------
@@ -427,7 +459,7 @@
          (string? collection)
          (map? database)]}
   (let [query   (if (string? query)
-                  (decode-iso-date-fncalls (json/parse-string (encode-iso-date-fncalls query) keyword))
+                  (decode-fncalls (json/parse-string (encode-fncalls query) keyword))
                   query)
         results (mc/aggregate *mongo-connection* collection query
                               :allow-disk-use true)
diff --git a/src/metabase/driver/oracle.clj b/src/metabase/driver/oracle.clj
index 61e52998955f35a5e82992bdbaad185810d2fb5d..1780dfe544ab3b995dcecacdefe58addb23f01a9 100644
--- a/src/metabase/driver/oracle.clj
+++ b/src/metabase/driver/oracle.clj
@@ -7,7 +7,6 @@
                       [helpers :as h])
             [metabase.config :as config]
             [toucan.db :as db]
-            [metabase.db.spec :as dbspec]
             [metabase.driver :as driver]
             [metabase.driver.generic-sql :as sql]
             [metabase.driver.generic-sql.query-processor :as sqlqp]
@@ -42,8 +41,15 @@
    [#"URI"         :type/Text]
    [#"XML"         :type/*]])
 
-(defn- connection-details->spec [{:keys [sid], :as details}]
-  (update (dbspec/oracle details) :subname (u/rpartial str \: sid)))
+(defn- connection-details->spec
+  "Create a database specification for an Oracle database. DETAILS should include keys
+  for `:user`, `:password`, and `:sid`. You can also optionally set `:host` and `:port`."
+  [{:keys [host port sid]
+    :or   {host "localhost", port 1521}
+    :as   details}]
+  (merge {:subprotocol "oracle:thin"
+          :subname     (str "@" host ":" port ":" sid)}
+         (dissoc details :host :port)))
 
 (defn- can-connect? [details]
   (let [connection (connection-details->spec details)]
diff --git a/src/metabase/driver/postgres.clj b/src/metabase/driver/postgres.clj
index 294ec44ee3c3119aa429fcbd8e22c2a0455cffcb..1e3c008d98d0ae41b3f60a92a8b710f7659f3fd7 100644
--- a/src/metabase/driver/postgres.clj
+++ b/src/metabase/driver/postgres.clj
@@ -180,23 +180,6 @@
       (isa? base-type :type/IPAddress) (hx/cast :inet value)
       :else                            value)))
 
-
-(defn- materialized-views
-  "Fetch the Materialized Views for a Postgres DATABASE.
-   These are returned as a set of maps, the same format as `:tables` returned by `describe-database`."
-  [database]
-  (try (set (jdbc/query (sql/db->jdbc-connection-spec database)
-                        ["SELECT schemaname AS \"schema\", matviewname AS \"name\" FROM pg_matviews;"]))
-       (catch Throwable e
-         (log/error "Failed to fetch materialized views for this database:" (.getMessage e)))))
-
-(defn- describe-database
-  "Custom implementation of `describe-database` for Postgres.
-   Postgres Materialized Views are not returned by normal JDBC methods: see [issue #2355](https://github.com/metabase/metabase/issues/2355); we have to manually fetch them.
-   This implementation combines the results from the generic SQL default implementation with materialized views fetched from `materialized-views`."
-  [driver database]
-  (update (sql/describe-database driver database) :tables (u/rpartial set/union (materialized-views database))))
-
 (defn- string-length-fn [field-key]
   (hsql/call :char_length (hx/cast :VARCHAR field-key)))
 
@@ -221,7 +204,6 @@
   driver/IDriver
   (merge (sql/IDriverSQLDefaultsMixin)
          {:date-interval                     (u/drop-first-arg date-interval)
-          :describe-database                 describe-database
           :details-fields                    (constantly [{:name         "host"
                                                            :display-name "Host"
                                                            :default      "localhost"}
diff --git a/src/metabase/driver/presto.clj b/src/metabase/driver/presto.clj
new file mode 100644
index 0000000000000000000000000000000000000000..97630c44ff7b08284a7b8d5a11700d335207def3
--- /dev/null
+++ b/src/metabase/driver/presto.clj
@@ -0,0 +1,344 @@
+(ns metabase.driver.presto
+  (:require [clojure.set :as set]
+            [clojure.string :as str]
+            [clj-http.client :as http]
+            (honeysql [core :as hsql]
+                      [helpers :as h])
+            [metabase.config :as config]
+            [metabase.driver :as driver]
+            [metabase.driver.generic-sql :as sql]
+            [metabase.driver.generic-sql.util.unprepare :as unprepare]
+            (metabase.models [field :as field]
+                             [table :as table])
+            [metabase.sync-database.analyze :as analyze]
+            [metabase.query-processor.util :as qputil]
+            [metabase.util :as u]
+            [metabase.util.honeysql-extensions :as hx])
+  (:import java.util.Date
+           (metabase.query_processor.interface DateTimeValue Value)))
+
+
+;;; Presto API helpers
+
+(defn- details->uri
+  [{:keys [ssl host port]} path]
+  (str (if ssl "https" "http") "://" host ":" port path))
+
+(defn- details->request [{:keys [user password catalog report-timezone]}]
+  (merge {:headers (merge {"X-Presto-Source" "metabase"
+                           "X-Presto-User"   user}
+                          (when catalog
+                            {"X-Presto-Catalog" catalog})
+                          (when report-timezone
+                            {"X-Presto-Time-Zone" report-timezone}))}
+         (when password
+           {:basic-auth [user password]})))
+
+(defn- parse-time-with-tz [s]
+  ;; Try parsing with offset first then with full ZoneId
+  (or (u/ignore-exceptions (u/parse-date "HH:mm:ss.SSS ZZ" s))
+      (u/parse-date "HH:mm:ss.SSS ZZZ" s)))
+
+(defn- parse-timestamp-with-tz [s]
+  ;; Try parsing with offset first then with full ZoneId
+  (or (u/ignore-exceptions (u/parse-date "yyyy-MM-dd HH:mm:ss.SSS ZZ" s))
+      (u/parse-date "yyyy-MM-dd HH:mm:ss.SSS ZZZ" s)))
+
+(defn- field-type->parser [field-type]
+  (condp re-matches field-type
+    #"decimal.*"                bigdec
+    #"time"                     (partial u/parse-date :hour-minute-second-ms)
+    #"time with time zone"      parse-time-with-tz
+    #"timestamp"                (partial u/parse-date "yyyy-MM-dd HH:mm:ss.SSS")
+    #"timestamp with time zone" parse-timestamp-with-tz
+    #".*"                       identity))
+
+(defn- parse-presto-results [columns data]
+  (let [parsers (map (comp field-type->parser :type) columns)]
+    (for [row data]
+      (for [[value parser] (partition 2 (interleave row parsers))]
+        (when value
+          (parser value))))))
+
+(defn- fetch-presto-results! [details {prev-columns :columns, prev-rows :rows} uri]
+  (let [{{:keys [columns data nextUri error]} :body} (http/get uri (assoc (details->request details) :as :json))]
+    (when error
+      (throw (ex-info (or (:message error) "Error running query.") error)))
+    (let [rows    (parse-presto-results columns data)
+          results {:columns (or columns prev-columns)
+                   :rows    (vec (concat prev-rows rows))}]
+      (if (nil? nextUri)
+        results
+        (do (Thread/sleep 100) ; Might not be the best way, but the pattern is that we poll Presto at intervals
+            (fetch-presto-results! details results nextUri))))))
+
+(defn- execute-presto-query! [details query]
+  (let [{{:keys [columns data nextUri error]} :body} (http/post (details->uri details "/v1/statement")
+                                                                (assoc (details->request details) :body query, :as :json))]
+    (when error
+      (throw (ex-info (or (:message error) "Error preparing query.") error)))
+    (let [rows    (parse-presto-results (or columns []) (or data []))
+          results {:columns (or columns [])
+                   :rows    rows}]
+      (if (nil? nextUri)
+        results
+        (fetch-presto-results! details results nextUri)))))
+
+
+;;; Generic helpers
+
+(defn- quote-name [nm]
+  (str \" (str/replace nm "\"" "\"\"") \"))
+
+(defn- quote+combine-names [& names]
+  (str/join \. (map quote-name names)))
+
+
+;;; IDriver implementation
+
+(defn- field-avg-length [{field-name :name, :as field}]
+  (let [table             (field/table field)
+        {:keys [details]} (table/database table)
+        sql               (format "SELECT cast(round(avg(length(%s))) AS integer) FROM %s WHERE %s IS NOT NULL"
+                            (quote-name field-name)
+                            (quote+combine-names (:schema table) (:name table))
+                            (quote-name field-name))
+        {[[v]] :rows}     (execute-presto-query! details sql)]
+    (or v 0)))
+
+(defn- field-percent-urls [{field-name :name, :as field}]
+  (let [table             (field/table field)
+        {:keys [details]} (table/database table)
+        sql               (format "SELECT cast(count_if(url_extract_host(%s) <> '') AS double) / cast(count(*) AS double) FROM %s WHERE %s IS NOT NULL"
+                            (quote-name field-name)
+                            (quote+combine-names (:schema table) (:name table))
+                            (quote-name field-name))
+        {[[v]] :rows}     (execute-presto-query! details sql)]
+    (if (= v "NaN") 0.0 v)))
+
+(defn- analyze-table [driver table new-table-ids]
+  ((analyze/make-analyze-table driver
+     :field-avg-length-fn   field-avg-length
+     :field-percent-urls-fn field-percent-urls) driver table new-table-ids))
+
+(defn- can-connect? [{:keys [catalog] :as details}]
+  (let [{[[v]] :rows} (execute-presto-query! details (str "SHOW SCHEMAS FROM " (quote-name catalog) " LIKE 'information_schema'"))]
+    (= v "information_schema")))
+
+(defn- date-interval [unit amount]
+  (hsql/call :date_add (hx/literal unit) amount :%now))
+
+(defn- describe-schema [{{:keys [catalog] :as details} :details} {:keys [schema]}]
+  (let [sql            (str "SHOW TABLES FROM " (quote+combine-names catalog schema))
+        {:keys [rows]} (execute-presto-query! details sql)
+        tables         (map first rows)]
+    (set (for [name tables]
+           {:name name, :schema schema}))))
+
+(defn- describe-database [{{:keys [catalog] :as details} :details :as database}]
+  (let [sql            (str "SHOW SCHEMAS FROM " (quote-name catalog))
+        {:keys [rows]} (execute-presto-query! details sql)
+        schemas        (remove #{"information_schema"} (map first rows))] ; inspecting "information_schema" breaks weirdly
+    {:tables (apply set/union (for [name schemas]
+                                (describe-schema database {:schema name})))}))
+
+(defn- presto-type->base-type [field-type]
+  (condp re-matches field-type
+    #"boolean"     :type/Boolean
+    #"tinyint"     :type/Integer
+    #"smallint"    :type/Integer
+    #"integer"     :type/Integer
+    #"bigint"      :type/BigInteger
+    #"real"        :type/Float
+    #"double"      :type/Float
+    #"decimal.*"   :type/Decimal
+    #"varchar.*"   :type/Text
+    #"char.*"      :type/Text
+    #"varbinary.*" :type/*
+    #"json"        :type/Text       ; TODO - this should probably be Dictionary or something
+    #"date"        :type/Date
+    #"time.*"      :type/DateTime
+    #"array"       :type/Array
+    #"map"         :type/Dictionary
+    #"row.*"       :type/*          ; TODO - again, but this time we supposedly have a schema
+    #".*"          :type/*))
+
+(defn- describe-table [{{:keys [catalog] :as details} :details} {schema :schema, table-name :name}]
+  (let [sql            (str "DESCRIBE " (quote+combine-names catalog schema table-name))
+        {:keys [rows]} (execute-presto-query! details sql)]
+    {:schema schema
+     :name   table-name
+     :fields (set (for [[name type] rows]
+                    {:name name, :base-type (presto-type->base-type type)}))}))
+
+(defprotocol ^:private IPrepareValue
+  (^:private prepare-value [this]))
+(extend-protocol IPrepareValue
+  nil           (prepare-value [_] nil)
+  DateTimeValue (prepare-value [{:keys [value]}] (prepare-value value))
+  Value         (prepare-value [{:keys [value]}] (prepare-value value))
+  String        (prepare-value [this] (hx/literal (str/replace this "'" "''")))
+  Boolean       (prepare-value [this] (hsql/raw (if this "TRUE" "FALSE")))
+  Date          (prepare-value [this] (hsql/call :from_iso8601_timestamp (hx/literal (u/date->iso-8601 this))))
+  Number        (prepare-value [this] this)
+  Object        (prepare-value [this] (throw (Exception. (format "Don't know how to prepare value %s %s" (class this) this)))))
+
+(defn- execute-query [{:keys [database settings], {sql :query, params :params} :native, :as outer-query}]
+  (let [sql                    (str "-- " (qputil/query->remark outer-query) "\n"
+                                          (unprepare/unprepare (cons sql params) :quote-escape "'", :iso-8601-fn  :from_iso8601_timestamp))
+        details                (merge (:details database) settings)
+        {:keys [columns rows]} (execute-presto-query! details sql)]
+    {:columns (map (comp keyword :name) columns)
+     :rows    rows}))
+
+(defn- field-values-lazy-seq [{field-name :name, :as field}]
+  ;; TODO - look into making this actually lazy
+  (let [table             (field/table field)
+        {:keys [details]} (table/database table)
+        sql               (format "SELECT %s FROM %s LIMIT %d"
+                            (quote-name field-name)
+                            (quote+combine-names (:schema table) (:name table))
+                            driver/max-sync-lazy-seq-results)
+        {:keys [rows]}    (execute-presto-query! details sql)]
+    (for [row rows]
+      (first row))))
+
+(defn- humanize-connection-error-message [message]
+  (condp re-matches message
+    #"^java.net.ConnectException: Connection refused.*$"
+    (driver/connection-error-messages :cannot-connect-check-host-and-port)
+
+    #"^clojure.lang.ExceptionInfo: Catalog .* does not exist.*$"
+    (driver/connection-error-messages :database-name-incorrect)
+
+    #"^java.net.UnknownHostException.*$"
+    (driver/connection-error-messages :invalid-hostname)
+
+    #".*" ; default
+    message))
+
+(defn- table-rows-seq [{:keys [details]} {:keys [schema name]}]
+  (let [sql                        (format "SELECT * FROM %s" (quote+combine-names schema name))
+        {:keys [rows], :as result} (execute-presto-query! details sql)
+        columns                    (map (comp keyword :name) (:columns result))]
+    (for [row rows]
+      (zipmap columns row))))
+
+
+;;; ISQLDriver implementation
+
+(defn- apply-page [honeysql-query {{:keys [items page]} :page}]
+  (let [offset (* (dec page) items)]
+    (if (zero? offset)
+      ;; if there's no offset we can simply use limit
+      (h/limit honeysql-query items)
+      ;; if we need to do an offset we have to do nesting to generate a row number and where on that
+      (let [over-clause (format "row_number() OVER (%s)"
+                                (first (hsql/format (select-keys honeysql-query [:order-by])
+                                                    :allow-dashed-names? true
+                                                    :quoting :ansi)))]
+        (-> (apply h/select (map last (:select honeysql-query)))
+            (h/from (h/merge-select honeysql-query [(hsql/raw over-clause) :__rownum__]))
+            (h/where [:> :__rownum__ offset])
+            (h/limit items))))))
+
+(defn- date [unit expr]
+  (case unit
+    :default         expr
+    :minute          (hsql/call :date_trunc (hx/literal :minute) expr)
+    :minute-of-hour  (hsql/call :minute expr)
+    :hour            (hsql/call :date_trunc (hx/literal :hour) expr)
+    :hour-of-day     (hsql/call :hour expr)
+    :day             (hsql/call :date_trunc (hx/literal :day) expr)
+    ;; Presto is ISO compliant, so we need to offset Monday = 1 to Sunday = 1
+    :day-of-week     (hx/+ (hx/mod (hsql/call :day_of_week expr) 7) 1)
+    :day-of-month    (hsql/call :day expr)
+    :day-of-year     (hsql/call :day_of_year expr)
+    ;; Similar to DoW, sicne Presto is ISO compliant the week starts on Monday, we need to shift that to Sunday
+    :week            (hsql/call :date_add (hx/literal :day) -1 (hsql/call :date_trunc (hx/literal :week) (hsql/call :date_add (hx/literal :day) 1 expr)))
+    ;; Offset by one day forward to "fake" a Sunday starting week
+    :week-of-year    (hsql/call :week (hsql/call :date_add (hx/literal :day) 1 expr))
+    :month           (hsql/call :date_trunc (hx/literal :month) expr)
+    :month-of-year   (hsql/call :month expr)
+    :quarter         (hsql/call :date_trunc (hx/literal :quarter) expr)
+    :quarter-of-year (hsql/call :quarter expr)
+    :year            (hsql/call :year expr)))
+
+(defn- string-length-fn [field-key]
+  (hsql/call :length field-key))
+
+(defn- unix-timestamp->timestamp [expr seconds-or-milliseconds]
+  (case seconds-or-milliseconds
+    :seconds      (hsql/call :from_unixtime expr)
+    :milliseconds (recur (hx// expr 1000.0) :seconds)))
+
+
+;;; Driver implementation
+
+(defrecord PrestoDriver []
+  clojure.lang.Named
+  (getName [_] "Presto"))
+
+(u/strict-extend PrestoDriver
+  driver/IDriver
+  (merge (sql/IDriverSQLDefaultsMixin)
+         {:analyze-table                     analyze-table
+          :can-connect?                      (u/drop-first-arg can-connect?)
+          :date-interval                     (u/drop-first-arg date-interval)
+          :describe-database                 (u/drop-first-arg describe-database)
+          :describe-table                    (u/drop-first-arg describe-table)
+          :describe-table-fks                (constantly nil) ; no FKs in Presto
+          :details-fields                    (constantly [{:name         "host"
+                                                           :display-name "Host"
+                                                           :default      "localhost"}
+                                                          {:name         "port"
+                                                           :display-name "Port"
+                                                           :type         :integer
+                                                           :default      8080}
+                                                          {:name         "catalog"
+                                                           :display-name "Database name"
+                                                           :placeholder  "hive"
+                                                           :required     true}
+                                                          {:name         "user"
+                                                           :display-name "Database username"
+                                                           :placeholder  "What username do you use to login to the database"
+                                                           :default      "metabase"}
+                                                          {:name         "password"
+                                                           :display-name "Database password"
+                                                           :type         :password
+                                                           :placeholder  "*******"}
+                                                          {:name         "ssl"
+                                                           :display-name "Use a secure connection (SSL)?"
+                                                           :type         :boolean
+                                                           :default      false}])
+          :execute-query                     (u/drop-first-arg execute-query)
+          :features                          (constantly (set/union #{:set-timezone
+                                                                      :basic-aggregations
+                                                                      :standard-deviation-aggregations
+                                                                      :expressions
+                                                                      :native-parameters
+                                                                      :expression-aggregations}
+                                                                    (when-not config/is-test?
+                                                                      ;; during unit tests don't treat presto as having FK support
+                                                                      #{:foreign-keys})))
+          :field-values-lazy-seq             (u/drop-first-arg field-values-lazy-seq)
+          :humanize-connection-error-message (u/drop-first-arg humanize-connection-error-message)
+          :table-rows-seq                    (u/drop-first-arg table-rows-seq)})
+
+  sql/ISQLDriver
+  (merge (sql/ISQLDriverDefaultsMixin)
+         {:apply-page                (u/drop-first-arg apply-page)
+          :column->base-type         (constantly nil)
+          :connection-details->spec  (constantly nil)
+          :current-datetime-fn       (constantly :%now)
+          :date                      (u/drop-first-arg date)
+          :excluded-schemas          (constantly #{"information_schema"})
+          :field-percent-urls        (u/drop-first-arg field-percent-urls)
+          :prepare-value             (u/drop-first-arg prepare-value)
+          :quote-style               (constantly :ansi)
+          :stddev-fn                 (constantly :stddev_samp)
+          :string-length-fn          (u/drop-first-arg string-length-fn)
+          :unix-timestamp->timestamp (u/drop-first-arg unix-timestamp->timestamp)}))
+
+
+(driver/register-driver! :presto (PrestoDriver.))
diff --git a/src/metabase/driver/sqlite.clj b/src/metabase/driver/sqlite.clj
index 7f90d9b6dc1d4213fb196e9ee06958657e88e1d6..3aa4092e92257da90cef4539c703ef22842d6a76 100644
--- a/src/metabase/driver/sqlite.clj
+++ b/src/metabase/driver/sqlite.clj
@@ -4,12 +4,22 @@
             (honeysql [core :as hsql]
                       [format :as hformat])
             [metabase.config :as config]
-            [metabase.db.spec :as dbspec]
             [metabase.driver :as driver]
             [metabase.driver.generic-sql :as sql]
             [metabase.util :as u]
             [metabase.util.honeysql-extensions :as hx]))
 
+(defn- connection-details->spec
+  "Create a database specification for a SQLite3 database. DETAILS should include a
+  key for `:db` which is the path to the database file."
+  [{:keys [db]
+    :or   {db "sqlite.db"}
+    :as   details}]
+  (merge {:classname   "org.sqlite.JDBC"
+          :subprotocol "sqlite"
+          :subname     db}
+         (dissoc details :db)))
+
 ;; We'll do regex pattern matching here for determining Field types
 ;; because SQLite types can have optional lengths, e.g. NVARCHAR(100) or NUMERIC(10,5)
 ;; See also http://www.sqlite.org/datatype3.html
@@ -158,7 +168,7 @@
   (merge (sql/ISQLDriverDefaultsMixin)
          {:active-tables             sql/post-filtered-active-tables
           :column->base-type         (sql/pattern-based-column->base-type pattern->type)
-          :connection-details->spec  (u/drop-first-arg dbspec/sqlite3)
+          :connection-details->spec  (u/drop-first-arg connection-details->spec)
           :current-datetime-fn       (constantly (hsql/call :datetime (hx/literal :now)))
           :date                      (u/drop-first-arg date)
           :prepare-sql-param         (u/drop-first-arg prepare-sql-param)
diff --git a/src/metabase/driver/sqlserver.clj b/src/metabase/driver/sqlserver.clj
index 140691c758465b28ae3d38d97068452268dd4550..e92f37a9711495e412d990a3cfae1eed3b7a2ec9 100644
--- a/src/metabase/driver/sqlserver.clj
+++ b/src/metabase/driver/sqlserver.clj
@@ -1,7 +1,6 @@
 (ns metabase.driver.sqlserver
   (:require [clojure.string :as s]
             [honeysql.core :as hsql]
-            [metabase.db.spec :as dbspec]
             [metabase.driver :as driver]
             [metabase.driver.generic-sql :as sql]
             [metabase.util :as u]
@@ -48,27 +47,26 @@
     :xml              :type/*
     (keyword "int identity") :type/Integer} column-type)) ; auto-incrementing integer (ie pk) field
 
-(defn- connection-details->spec [{:keys [domain instance ssl], :as details}]
-  (-> ;; Having the `:ssl` key present, even if it is `false`, will make the driver attempt to connect with SSL
-      (dbspec/mssql (if ssl
-                      details
-                      (dissoc details :ssl)))
-      ;; swap out Microsoft Driver details for jTDS ones
-      (assoc :classname   "net.sourceforge.jtds.jdbc.Driver"
-             :subprotocol "jtds:sqlserver")
-
-      ;; adjust the connection URL to match up with the jTDS format (see http://jtds.sourceforge.net/faq.html#urlFormat)
-      (update :subname (fn [subname]
-                         ;; jTDS uses a "/" instead of ";database="
-                         (cond-> (s/replace subname #";database=" "/")
-                           ;; and add the ;instance= option if applicable
-                           (seq instance) (str ";instance=" instance)
-
-                           ;; add Windows domain for Windows domain authentication if applicable. useNTLMv2 = send LMv2/NTLMv2 responses when using Windows auth
-                           (seq domain) (str ";domain=" domain ";useNTLMv2=true")
-
-                           ;; If SSL is specified append ;ssl=require, which enables SSL and throws exception if SSL connection cannot be made
-                           ssl (str ";ssl=require"))))))
+
+(defn- connection-details->spec [{:keys [user password db host port instance domain ssl]
+                                  :or   {user "dbuser", password "dbpassword", db "", host "localhost", port 1433}
+                                  :as   details}]
+  {:classname    "net.sourceforge.jtds.jdbc.Driver"
+   :subprotocol  "jtds:sqlserver"
+   :loginTimeout 5 ; Wait up to 10 seconds for connection success. If we get no response by then, consider the connection failed
+   :subname      (str "//" host ":" port "/" db)
+   ;; everything else gets passed as `java.util.Properties` to the JDBC connection. See full list of properties here: `http://jtds.sourceforge.net/faq.html#urlFormat`
+   ;; (passing these as Properties instead of part of the `:subname` is preferable because they support things like passwords with special characters)
+   :user         user
+   :password     password
+   :instance     instance
+   :domain       domain
+   :useNTLMv2    (boolean domain) ; if domain is specified, send LMv2/NTLMv2 responses when using Windows authentication
+   ;; for whatever reason `ssl=request` doesn't work with RDS (it hangs indefinitely), so just set ssl=off (disabled) if SSL isn't being used
+   :ssl          (if ssl
+                   "require"
+                   "off")})
+
 
 (defn- date-part [unit expr]
   (hsql/call :datepart (hsql/raw (name unit)) expr))
diff --git a/src/metabase/driver/vertica.clj b/src/metabase/driver/vertica.clj
index 53ae8dd76afa9ac2d85ba9dfc6e7a3b9f5b6abb2..675e815ff7aa97d1a1c88205ab561a3663638332 100644
--- a/src/metabase/driver/vertica.clj
+++ b/src/metabase/driver/vertica.clj
@@ -32,22 +32,13 @@
    (keyword "Long Varchar")   :type/Text
    (keyword "Long Varbinary") :type/*})
 
-(defn- vertica-spec [{:keys [host port db]
-                      :or   {host "localhost", port 5433, db ""}
-                      :as   opts}]
+(defn- connection-details->spec [{:keys [host port db dbname]
+                                  :or   {host "localhost", port 5433, db ""}
+                                  :as   details}]
   (merge {:classname   "com.vertica.jdbc.Driver"
           :subprotocol "vertica"
-          :subname     (str "//" host ":" port "/" db)}
-         (dissoc opts :host :port :db :ssl)))
-
-(defn- connection-details->spec [details-map]
-  (-> details-map
-      (update :port (fn [port]
-                      (if (string? port)
-                        (Integer/parseInt port)
-                        port)))
-      (rename-keys {:dbname :db})
-      vertica-spec))
+          :subname     (str "//" host ":" port "/" (or dbname db))}
+         (dissoc details :host :port :dbname :db :ssl)))
 
 (defn- unix-timestamp->timestamp [expr seconds-or-milliseconds]
   (case seconds-or-milliseconds
diff --git a/src/metabase/email.clj b/src/metabase/email.clj
index 0bbc09ffc3fb8b3a32d263a82ad25fdbf0ec3499..9731b50acdbdae802f760dda254ccf090546ef74 100644
--- a/src/metabase/email.clj
+++ b/src/metabase/email.clj
@@ -6,7 +6,8 @@
             [metabase.util :as u])
   (:import javax.mail.Session))
 
-;; ## CONFIG
+;;; CONFIG
+;; TODO - smtp-port should be switched to type :integer
 
 (defsetting email-from-address  "Email address you want to use as the sender of Metabase." :default "notifications@metabase.com")
 (defsetting email-smtp-host     "The address of the SMTP server that handles your emails.")
diff --git a/src/metabase/integrations/slack.clj b/src/metabase/integrations/slack.clj
index 562d8710f7f769eee3a93ed31296e9862fb69d1f..2bcc5dc823eb681821e7a44cc0ddae629e6589f6 100644
--- a/src/metabase/integrations/slack.clj
+++ b/src/metabase/integrations/slack.clj
@@ -37,14 +37,6 @@
 (def ^{:arglists '([endpoint & {:as params}]), :style/indent 1} GET  "Make a GET request to the Slack API."  (partial do-slack-request http/get  :query-params))
 (def ^{:arglists '([endpoint & {:as params}]), :style/indent 1} POST "Make a POST request to the Slack API." (partial do-slack-request http/post :form-params))
 
-(def ^:private ^{:arglists '([channel-id & {:as args}])} create-channel!
-  "Calls Slack api `channels.create` for CHANNEL."
-  (partial POST :channels.create, :name))
-
-(def ^:private ^{:arglists '([channel-id & {:as args}])} archive-channel!
-  "Calls Slack api `channels.archive` for CHANNEL."
-  (partial POST :channels.archive, :channel))
-
 (def ^{:arglists '([& {:as args}])} channels-list
   "Calls Slack api `channels.list` function and returns the list of available channels."
   (comp :channels (partial GET :channels.list, :exclude_archived 1)))
@@ -53,29 +45,26 @@
   "Calls Slack api `users.list` function and returns the list of available users."
   (comp :members (partial GET :users.list)))
 
-(defn- create-files-channel!
-  "Convenience function for creating our Metabase files channel to store file uploads."
-  []
-  (when-let [{files-channel :channel, :as response} (create-channel! files-channel-name)]
-    (when-not files-channel
-      (log/error (u/pprint-to-str 'red response))
-      (throw (ex-info "Error creating Slack channel for Metabase file uploads" response)))
-    ;; Right after creating our files channel, archive it. This is because we don't need users to see it.
-    (u/prog1 files-channel
-      (archive-channel! (:id <>)))))
-
-(defn- files-channel
-  "Return the `metabase_files` channel (as a map) if it exists."
+(def ^:private ^:const ^String channel-missing-msg
+  (str "Slack channel named `metabase_files` is missing! Please create the channel in order to complete "
+       "the Slack integration. The channel is used for storing graphs that are included in pulses and "
+       "MetaBot answers."))
+
+(defn- maybe-get-files-channel
+  "Return the `metabase_files channel (as a map) if it exists."
   []
   (some (fn [channel] (when (= (:name channel) files-channel-name)
                         channel))
         (channels-list :exclude_archived 0)))
 
-(defn get-or-create-files-channel!
-  "Calls Slack api `channels.info` and `channels.create` function as needed to ensure that a #metabase_files channel exists."
+(defn files-channel
+  "Calls Slack api `channels.info` to check whether a channel named #metabase_files exists. If it doesn't,
+   throws an error that advices an admin to create it."
   []
-  (or (files-channel)
-      (create-files-channel!)))
+  (or (maybe-get-files-channel)
+      (do (log/error (u/format-color 'red channel-missing-msg))
+          (throw (ex-info channel-missing-msg {:status-code 400})))))
+
 
 (defn upload-file!
   "Calls Slack api `files.upload` function and returns the body of the uploaded file."
diff --git a/src/metabase/middleware.clj b/src/metabase/middleware.clj
index b1b99bd291c9f8c76b82de5c1a0890fd0de98c64..88f19c572b8683105bff19f4bfbbbbf5b27c4800 100644
--- a/src/metabase/middleware.clj
+++ b/src/metabase/middleware.clj
@@ -9,6 +9,7 @@
             [metabase.api.common :refer [*current-user* *current-user-id* *is-superuser?* *current-user-permissions-set*]]
             [metabase.api.common.internal :refer [*automatically-catch-api-exceptions*]]
             [metabase.config :as config]
+            [metabase.core.initialization-status :as init-status]
             [metabase.db :as mdb]
             (metabase.models [session :refer [Session]]
                              [setting :refer [defsetting]]
@@ -89,19 +90,17 @@
 (defn- current-user-info-for-session
   "Return User ID and superuser status for Session with SESSION-ID if it is valid and not expired."
   [session-id]
-  (when (and session-id (or ((resolve 'metabase.core/initialized?))
-                            (println "Metabase is not initialized!") ; NOCOMMIT
-                            ))
+  (when (and session-id (init-status/complete?))
     (when-let [session (or (session-with-id session-id)
-                           (println "no matching session with ID") ; NOCOMMIT
+                           (println "no matching session with ID") ; DEBUG
                            )]
       (if (session-expired? session)
-        (println (format "session-is-expired! %d min / %d min" (session-age-minutes session) (config/config-int :max-session-age))) ; NOCOMMIT
+        (printf "session-is-expired! %d min / %d min\n" (session-age-minutes session) (config/config-int :max-session-age)) ; DEBUG
         {:metabase-user-id (:user_id session)
          :is-superuser?    (:is_superuser session)}))))
 
 (defn- add-current-user-info [{:keys [metabase-session-id], :as request}]
-  (when-not ((resolve 'metabase.core/initialized?))
+  (when-not (init-status/complete?)
     (println "Metabase is not initialized yet!")) ; DEBUG
   (merge request (current-user-info-for-session metabase-session-id)))
 
@@ -191,11 +190,9 @@
                                                                     "https://www.google-analytics.com" ; Safari requires the protocol
                                                                     "https://*.googleapis.com"
                                                                     "*.gstatic.com"
-                                                                    "js.intercomcdn.com"
-                                                                    "*.intercom.io"
                                                                     (when config/is-dev?
                                                                       "localhost:8080")]
-                                                      :frame-src   ["'self'"
+                                                      :child-src   ["'self'"
                                                                     "https://accounts.google.com"] ; TODO - double check that we actually need this for Google Auth
                                                       :style-src   ["'unsafe-inline'"
                                                                     "'self'"
@@ -206,11 +203,9 @@
                                                                     (when config/is-dev?
                                                                       "localhost:8080")]
                                                       :img-src     ["*"
-                                                                    "self data:"]
+                                                                    "'self' data:"]
                                                       :connect-src ["'self'"
                                                                     "metabase.us10.list-manage.com"
-                                                                    "*.intercom.io"
-                                                                    "wss://*.intercom.io" ; allow websockets as well
                                                                     (when config/is-dev?
                                                                       "localhost:8080 ws://localhost:8080")]}]
                                           (format "%s %s; " (name k) (apply str (interpose " " vs)))))})
@@ -261,10 +256,11 @@
   "Middleware to set the `site-url` Setting if it's unset the first time a request is made."
   [handler]
   (fn [{{:strs [origin host] :as headers} :headers, :as request}]
-    (when-not (public-settings/site-url)
-      (when-let [site-url (or origin host)]
-        (log/info "Setting Metabase site URL to" site-url)
-        (public-settings/site-url site-url)))
+    (when (mdb/db-is-setup?)
+      (when-not (public-settings/site-url)
+        (when-let [site-url (or origin host)]
+          (log/info "Setting Metabase site URL to" site-url)
+          (public-settings/site-url site-url))))
     (handler request)))
 
 
diff --git a/src/metabase/models/field_values.clj b/src/metabase/models/field_values.clj
index 8150b974998114bdb611d4ce429de1b395981465..2a8fcf8751e8d5c86c53eb517127e9ad2d35d6fd 100644
--- a/src/metabase/models/field_values.clj
+++ b/src/metabase/models/field_values.clj
@@ -54,15 +54,14 @@
   {:pre [(integer? field-id)
          (field-should-have-field-values? field)]}
   (if-let [field-values (FieldValues :field_id field-id)]
-    (db/update! FieldValues (:id field-values)
+    (db/update! FieldValues (u/get-id field-values)
       :values ((resolve 'metabase.db.metadata-queries/field-distinct-values) field))
     (create-field-values! field)))
 
 (defn create-field-values-if-needed!
   "Create `FieldValues` for a `Field` if they *should* exist but don't already exist.
    Returns the existing or newly created `FieldValues` for `Field`."
-  {:arglists '([field]
-               [field human-readable-values])}
+  {:arglists '([field] [field human-readable-values])}
   [{field-id :id :as field} & [human-readable-values]]
   {:pre [(integer? field-id)]}
   (when (field-should-have-field-values? field)
@@ -72,10 +71,9 @@
 (defn save-field-values!
   "Save the `FieldValues` for FIELD-ID, creating them if needed, otherwise updating them."
   [field-id values]
-  {:pre [(integer? field-id)
-         (coll? values)]}
+  {:pre [(integer? field-id) (coll? values)]}
   (if-let [field-values (FieldValues :field_id field-id)]
-    (db/update! FieldValues (:id field-values), :values values)
+    (db/update! FieldValues (u/get-id field-values), :values values)
     (db/insert! FieldValues :field_id field-id, :values values)))
 
 (defn clear-field-values!
diff --git a/src/metabase/models/interface.clj b/src/metabase/models/interface.clj
index 804877b9dd93f5c4cba623b15d2d48aa53bb7016..308210288029306ae3d095a390eb1a00a84bfffd 100644
--- a/src/metabase/models/interface.clj
+++ b/src/metabase/models/interface.clj
@@ -1,15 +1,20 @@
 (ns metabase.models.interface
   (:require [clojure.core.memoize :as memoize]
             [cheshire.core :as json]
+            [taoensso.nippy :as nippy]
             [toucan.models :as models]
             [metabase.config :as config]
             [metabase.util :as u]
-            [metabase.util.encryption :as encryption]))
+            [metabase.util.encryption :as encryption])
+  (:import java.sql.Blob))
 
 ;;; ------------------------------------------------------------ Toucan Extensions ------------------------------------------------------------
 
 (models/set-root-namespace! 'metabase.models)
 
+
+;;; types
+
 (defn- json-in [obj]
   (if (string? obj)
     obj
@@ -39,6 +44,24 @@
   :in  encrypted-json-in
   :out (comp cached-encrypted-json-out u/jdbc-clob->str))
 
+(defn compress
+  "Compress OBJ, returning a byte array."
+  [obj]
+  (nippy/freeze obj {:compressor nippy/snappy-compressor}))
+
+(defn decompress
+  "Decompress COMPRESSED-BYTES."
+  [compressed-bytes]
+  (if (instance? Blob compressed-bytes)
+    (recur (.getBytes ^Blob compressed-bytes 0 (.length ^Blob compressed-bytes)))
+    (nippy/thaw compressed-bytes {:compressor nippy/snappy-compressor})))
+
+(models/add-type! :compressed
+  :in  compress
+  :out decompress)
+
+
+;;; properties
 
 (defn- add-created-at-timestamp [obj & _]
   (assoc obj :created_at (u/new-sql-timestamp)))
@@ -50,6 +73,11 @@
   :insert (comp add-created-at-timestamp add-updated-at-timestamp)
   :update add-updated-at-timestamp)
 
+;; like `timestamped?`, but for models that only have an `:updated_at` column
+(models/add-property! :updated-at-timestamped?
+  :insert add-updated-at-timestamp
+  :update add-updated-at-timestamp)
+
 
 ;;; ------------------------------------------------------------ New Permissions Stuff ------------------------------------------------------------
 
diff --git a/src/metabase/models/pulse.clj b/src/metabase/models/pulse.clj
index e455e1f0666f7f25c5e55243aebfc036fa67754e..7993cd7b01c078e025da215fa932edc27a631f8f 100644
--- a/src/metabase/models/pulse.clj
+++ b/src/metabase/models/pulse.clj
@@ -168,7 +168,7 @@
   `PulseCards`, `PulseChannels`, and `PulseChannelRecipients`.
 
    Returns the newly created `Pulse` or throws an Exception."
-  [pulse-name creator-id card-ids channels]
+  [pulse-name creator-id card-ids channels skip-if-empty?]
   {:pre [(string? pulse-name)
          (integer? creator-id)
          (sequential? card-ids)
@@ -179,7 +179,8 @@
   (db/transaction
     (let [{:keys [id] :as pulse} (db/insert! Pulse
                                    :creator_id creator-id
-                                   :name pulse-name)]
+                                   :name pulse-name
+                                   :skip_if_empty skip-if-empty?)]
       ;; add card-ids to the Pulse
       (update-pulse-cards! pulse card-ids)
       ;; add channels to the Pulse
@@ -192,7 +193,7 @@
   "Update an existing `Pulse`, including all associated data such as: `PulseCards`, `PulseChannels`, and `PulseChannelRecipients`.
 
    Returns the updated `Pulse` or throws an Exception."
-  [{:keys [id name cards channels] :as pulse}]
+  [{:keys [id name cards channels skip-if-empty?] :as pulse}]
   {:pre [(integer? id)
          (string? name)
          (sequential? cards)
@@ -202,7 +203,7 @@
          (every? map? channels)]}
   (db/transaction
     ;; update the pulse itself
-    (db/update! Pulse id, :name name)
+    (db/update! Pulse id, :name name, :skip_if_empty skip-if-empty?)
     ;; update cards (only if they changed)
     (when (not= cards (map :card_id (db/select [PulseCard :card_id], :pulse_id id, {:order-by [[:position :asc]]})))
       (update-pulse-cards! pulse cards))
diff --git a/src/metabase/models/query.clj b/src/metabase/models/query.clj
new file mode 100644
index 0000000000000000000000000000000000000000..74c80cba809607118cb40df8ac24b4ee3354e5be
--- /dev/null
+++ b/src/metabase/models/query.clj
@@ -0,0 +1,41 @@
+(ns metabase.models.query
+  (:require (toucan [db :as db]
+                    [models :as models])
+            [metabase.db :as mdb]
+            [metabase.util :as u]
+            [metabase.util.honeysql-extensions :as hx]))
+
+(models/defmodel Query :query)
+
+;;; Helper Fns
+
+(defn average-execution-time-ms
+  "Fetch the average execution time (in milliseconds) for query with QUERY-HASH if available.
+   Returns `nil` if no information is available."
+  ^Integer [^bytes query-hash]
+  {:pre [(instance? (Class/forName "[B") query-hash)]}
+  (db/select-one-field :average_execution_time Query :query_hash query-hash))
+
+(defn- int-casting-type
+  "Return appropriate type for use in SQL `CAST(x AS type)` statement.
+   MySQL doesn't accept `integer`, so we have to use `unsigned`; Postgres doesn't accept `unsigned`.
+   so we have to use `integer`. Yay SQL dialect differences :D"
+  []
+  (if (= (mdb/db-type) :mysql)
+    :unsigned
+    :integer))
+
+(defn update-average-execution-time!
+  "Update the recorded average execution time for query with QUERY-HASH."
+  ^Integer [^bytes query-hash, ^Integer execution-time-ms]
+  {:pre [(instance? (Class/forName "[B") query-hash)]}
+  (or
+   ;; if there's already a matching Query update the rolling average
+   (db/update-where! Query {:query_hash query-hash}
+     :average_execution_time (hx/cast (int-casting-type) (hx/round (hx/+ (hx/* 0.9 :average_execution_time)
+                                                                         (*    0.1 execution-time-ms))
+                                                                   0)))
+   ;; otherwise add a new entry, using the value of EXECUTION-TIME-MS as a starting point
+   (db/insert! Query
+     :query_hash             query-hash
+     :average_execution_time execution-time-ms)))
diff --git a/src/metabase/models/query_cache.clj b/src/metabase/models/query_cache.clj
new file mode 100644
index 0000000000000000000000000000000000000000..e4277919d641cb58629f75311f2da3093c13794a
--- /dev/null
+++ b/src/metabase/models/query_cache.clj
@@ -0,0 +1,12 @@
+(ns metabase.models.query-cache
+  "A model used to cache query results in the database."
+  (:require [toucan.models :as models]
+            [metabase.util :as u]))
+
+(models/defmodel QueryCache :query_cache)
+
+(u/strict-extend (class QueryCache)
+  models/IModel
+  (merge models/IModelDefaults
+         {:types      (constantly {:results :compressed})
+          :properties (constantly {:updated-at-timestamped? true})}))
diff --git a/src/metabase/models/setting.clj b/src/metabase/models/setting.clj
index 9595bc32802fcf6fea124874f14ce19f53667bd8..713a2b92f49c6ae73399e1fd6b0d55835e7aaaca 100644
--- a/src/metabase/models/setting.clj
+++ b/src/metabase/models/setting.clj
@@ -31,6 +31,7 @@
       (setting/all)"
   (:refer-clojure :exclude [get])
   (:require [clojure.string :as str]
+            [clojure.tools.logging :as log]
             [cheshire.core :as json]
             [environ.core :as env]
             [medley.core :as m]
@@ -52,7 +53,7 @@
 
 
 (def ^:private Type
-  (s/enum :string :boolean :json))
+  (s/enum :string :boolean :json :integer))
 
 (def ^:private SettingDefinition
   {:name        s/Keyword
@@ -81,7 +82,7 @@
 ;; Cache is a 1:1 mapping of what's in the DB
 ;; Cached lookup time is ~60µs, compared to ~1800µs for DB lookup
 
-(def ^:private cache
+(defonce ^:private cache
   (atom nil))
 
 (defn- restore-cache-if-needed! []
@@ -151,6 +152,12 @@
   ^Boolean [setting-or-name]
   (string->boolean (get-string setting-or-name)))
 
+(defn get-integer
+  "Get integer value of (presumably `:integer`) SETTING-OR-NAME. This is the default getter for `:integer` settings."
+  ^Integer [setting-or-name]
+  (when-let [s (get-string setting-or-name)]
+    (Integer/parseInt s)))
+
 (defn get-json
   "Get the string value of SETTING-OR-NAME and parse it as JSON."
   [setting-or-name]
@@ -159,6 +166,7 @@
 (def ^:private default-getter-for-type
   {:string  get-string
    :boolean get-boolean
+   :integer get-integer
    :json    get-json})
 
 (defn get
@@ -170,6 +178,24 @@
 
 ;;; ------------------------------------------------------------ set! ------------------------------------------------------------
 
+(defn- update-setting! [setting-name new-value]
+  (db/update-where! Setting {:key setting-name}
+    :value new-value))
+
+(defn- set-new-setting!
+  "Insert a new row for a Setting with SETTING-NAME and SETTING-VALUE.
+   Takes care of resetting the cache if the insert fails for some reason."
+  [setting-name new-value]
+  (try (db/insert! Setting
+         :key   setting-name
+         :value new-value)
+       ;; if for some reason inserting the new value fails it almost certainly means the cache is out of date
+       ;; and there's actually a row in the DB that's not in the cache for some reason. Go ahead and update the
+       ;; existing value and log a warning
+       (catch Throwable e
+         (log/warn "Error INSERTing a new Setting:" (.getMessage e) "\nAssuming Setting already exists in DB and updating existing value.")
+         (update-setting! setting-name new-value))))
+
 (s/defn ^:always-validate set-string!
   "Set string value of SETTING-OR-NAME. A `nil` or empty NEW-VALUE can be passed to unset (i.e., delete) SETTING-OR-NAME."
   [setting-or-name, new-value :- (s/maybe s/Str)]
@@ -182,12 +208,9 @@
     (cond
       (not new-value)                 (db/simple-delete! Setting :key setting-name)
       ;; if there's a value in the cache then the row already exists in the DB; update that
-      (contains? @cache setting-name) (db/update-where! Setting {:key setting-name}
-                                        :value new-value)
+      (contains? @cache setting-name) (update-setting! setting-name new-value)
       ;; if there's nothing in the cache then the row doesn't exist, insert a new one
-      :else                           (db/insert! Setting
-                                        :key  setting-name
-                                        :value new-value))
+      :else                           (set-new-setting! setting-name new-value))
     ;; update cached value
     (if new-value
       (swap! cache assoc  setting-name new-value)
@@ -204,6 +227,15 @@
                                    false "false"
                                    nil   nil))))
 
+(defn set-integer!
+  "Set the value of integer SETTING-OR-NAME."
+  [setting-or-name new-value]
+  (set-string! setting-or-name (when new-value
+                                 (assert (or (integer? new-value)
+                                             (and (string? new-value)
+                                                  (re-matches #"^\d+$" new-value))))
+                                 (str new-value))))
+
 (defn set-json!
   "Serialize NEW-VALUE for SETTING-OR-NAME as a JSON string and save it."
   [setting-or-name new-value]
@@ -213,6 +245,7 @@
 (def ^:private default-setter-for-type
   {:string  set-string!
    :boolean set-boolean!
+   :integer set-integer!
    :json    set-json!})
 
 (defn set!
@@ -300,7 +333,7 @@
    You may optionally pass any of the OPTIONS below:
 
    *  `:default` - The default value of the setting. (default: `nil`)
-   *  `:type` - `:string` (default), `:boolean`, or `:json`. Non-`:string` settings have special default getters and setters that automatically coerce values to the correct types.
+   *  `:type` - `:string` (default), `:boolean`, `:integer`, or `:json`. Non-`:string` settings have special default getters and setters that automatically coerce values to the correct types.
    *  `:internal?` - This `Setting` is for internal use and shouldn't be exposed in the UI (i.e., not
                      returned by the corresponding endpoints). Default: `false`
    *  `:getter` - A custom getter fn, which takes no arguments. Overrides the default implementation.
diff --git a/src/metabase/public_settings.clj b/src/metabase/public_settings.clj
index 35f5ae33027abfff85425f7d959934a8c7c62b29..4d9900614fa575d603f9838e7abeefbc22074568 100644
--- a/src/metabase/public_settings.clj
+++ b/src/metabase/public_settings.clj
@@ -23,11 +23,13 @@
   :default "Metabase")
 
 ;; This value is *guaranteed* to never have a trailing slash :D
+;; It will also prepend `http://` to the URL if there's not protocol when it comes in
 (defsetting site-url
   "The base URL of this Metabase instance, e.g. \"http://metabase.my-company.com\"."
   :setter (fn [new-value]
             (setting/set-string! :site-url (when new-value
-                                             (s/replace new-value #"/$" "")))))
+                                             (cond->> (s/replace new-value #"/$" "")
+                                               (not (s/starts-with? new-value "http")) (str "http://"))))))
 
 (defsetting admin-email
   "The email address users should be referred to if they encounter a problem.")
@@ -51,6 +53,40 @@
   :type    :boolean
   :default false)
 
+
+(defsetting enable-query-caching
+  "Enabling caching will save the results of queries that take a long time to run."
+  :type    :boolean
+  :default false)
+
+(defsetting query-caching-max-kb
+  "The maximum size of the cache per card, in kilobytes:"
+  ;; (This size is a measurement of the length of *uncompressed* serialized result *rows*. The actual size of
+  ;; the results as stored will vary somewhat, since this measurement doesn't include metadata returned with the
+  ;; results, and doesn't consider whether the results are compressed, as the `:db` backend does.)
+  :type    :integer
+  :default 1000)
+
+(defsetting query-caching-max-ttl
+  "The absoulte maximum time to keep any cached query results, in seconds."
+  :type    :integer
+  :default (* 60 60 24 100)) ; 100 days
+
+(defsetting query-caching-min-ttl
+  "Metabase will cache all saved questions with an average query execution time longer than
+   this many seconds:"
+  :type    :integer
+  :default 60)
+
+(defsetting query-caching-ttl-ratio
+  "To determine how long each saved question's cached result should stick around, we take the
+   query's average execution time and multiply that by whatever you input here. So if a query
+   takes on average 2 minutes to run, and you input 10 for your multiplier, its cache entry
+   will persist for 20 minutes."
+  :type    :integer
+  :default 10)
+
+
 (defn remove-public-uuid-if-public-sharing-is-disabled
   "If public sharing is *disabled* and OBJECT has a `:public_uuid`, remove it so people don't try to use it (since it won't work).
    Intended for use as part of a `post-select` implementation for Cards and Dashboards."
@@ -79,6 +115,7 @@
    :anon_tracking_enabled (anon-tracking-enabled)
    :custom_geojson        (setting/get :custom-geojson)
    :email_configured      ((resolve 'metabase.email/email-configured?))
+   :enable_query_caching  (enable-query-caching)
    :engines               ((resolve 'metabase.driver/available-drivers))
    :ga_code               "UA-60817802-1"
    :google_auth_client_id (setting/get :google-auth-client-id)
diff --git a/src/metabase/pulse.clj b/src/metabase/pulse.clj
index adff446e58607c1b5afa13322bd88dab5a89c1b7..2a1b81b901430bd397af93f019551c1daccdaefc 100644
--- a/src/metabase/pulse.clj
+++ b/src/metabase/pulse.clj
@@ -46,7 +46,7 @@
 (defn create-and-upload-slack-attachments!
   "Create an attachment in Slack for a given Card by rendering its result into an image and uploading it."
   [card-results]
-  (when-let [{channel-id :id} (slack/get-or-create-files-channel!)]
+  (let [{channel-id :id} (slack/files-channel)]
     (doall (for [{{card-id :id, card-name :name, :as card} :card, result :result} card-results]
              (let [image-byte-array (render/render-pulse-card-to-png card result)
                    slack-file-url   (slack/upload-file! image-byte-array "image.png" channel-id)]
@@ -65,6 +65,20 @@
                               (str "Pulse: " (:name pulse))
                               attachments)))
 
+(defn- is-card-empty?
+  "Check if the card is empty"
+  [card]
+  (let [result (:result card)]
+    (or (zero? (-> result :row_count))
+        ;; Many aggregations result in [[nil]] if there are no rows to aggregate after filters
+        (= [[nil]]
+           (-> result :data :rows)))))
+
+(defn- are-all-cards-empty?
+  "Do none of the cards have any results?"
+  [results]
+  (every? is-card-empty? results))
+
 (defn send-pulse!
   "Execute and Send a `Pulse`, optionally specifying the specific `PulseChannels`.  This includes running each
    `PulseCard`, formatting the results, and sending the results to any specified destination.
@@ -77,9 +91,10 @@
   (let [results     (for [card cards]
                       (execute-card (:id card), :pulse-id (:id pulse))) ; Pulse ID may be `nil` if the Pulse isn't saved yet
         channel-ids (or channel-ids (mapv :id (:channels pulse)))]
-    (doseq [channel-id channel-ids]
-      (let [{:keys [channel_type details recipients]} (some #(when (= channel-id (:id %)) %)
-                                                            (:channels pulse))]
-        (condp = (keyword channel_type)
-          :email (send-email-pulse! pulse results recipients)
-          :slack (send-slack-pulse! pulse results (:channel details)))))))
+    (when-not (and (:skip_if_empty pulse) (are-all-cards-empty? results))
+      (doseq [channel-id channel-ids]
+        (let [{:keys [channel_type details recipients]} (some #(when (= channel-id (:id %)) %)
+                                                              (:channels pulse))]
+          (condp = (keyword channel_type)
+            :email (send-email-pulse! pulse results recipients)
+            :slack (send-slack-pulse! pulse results (:channel details))))))))
diff --git a/src/metabase/pulse/render.clj b/src/metabase/pulse/render.clj
index e988b311e61bcd4b7403247320d4e87d72359fc1..26dadd4ce6293e17b1064eaadedf3b5ad6531bf8 100644
--- a/src/metabase/pulse/render.clj
+++ b/src/metabase/pulse/render.clj
@@ -390,7 +390,9 @@
     (cond
       (or (= aggregation :rows)
           (contains? #{:pin_map :state :country} (:display card))) nil
-      (zero? row-count)                                            :empty
+      (or (zero? row-count)
+          ;; Many aggregations result in [[nil]] if there are no rows to aggregate after filters
+          (= [[nil]] (-> data :rows)))                             :empty
       (and (= col-count 1)
            (= row-count 1))                                        :scalar
       (and (= col-count 2)
diff --git a/src/metabase/query_processor.clj b/src/metabase/query_processor.clj
index f08dfa5b4bee3857c06b897dce41b4a81b8fcf7b..834d312848664a35d1e38417a565cc2ed27cb2cf 100644
--- a/src/metabase/query_processor.clj
+++ b/src/metabase/query_processor.clj
@@ -4,13 +4,15 @@
             [schema.core :as s]
             [toucan.db :as db]
             [metabase.driver :as driver]
-            [metabase.models.query-execution :refer [QueryExecution], :as query-execution]
+            (metabase.models [query :as query]
+                             [query-execution :refer [QueryExecution], :as query-execution])
             [metabase.query-processor.util :as qputil]
             (metabase.query-processor.middleware [add-implicit-clauses :as implicit-clauses]
                                                  [add-row-count-and-status :as row-count-and-status]
                                                  [add-settings :as add-settings]
                                                  [annotate-and-sort :as annotate-and-sort]
                                                  [catch-exceptions :as catch-exceptions]
+                                                 [cache :as cache]
                                                  [cumulative-aggregations :as cumulative-ags]
                                                  [dev :as dev]
                                                  [driver-specific :as driver-specific]
@@ -79,8 +81,9 @@
        driver-specific/process-query-in-context      ; (drivers can inject custom middleware if they implement IDriver's `process-query-in-context`)
        add-settings/add-settings
        resolve-driver/resolve-driver                 ; ▲▲▲ DRIVER RESOLUTION POINT ▲▲▲ All functions *above* will have access to the driver during PRE- *and* POST-PROCESSING
-       catch-exceptions/catch-exceptions
-       log-query/log-initial-query)
+       log-query/log-initial-query
+       cache/maybe-return-cached-results
+       catch-exceptions/catch-exceptions)
    query))
 ;; ▲▲▲ PRE-PROCESSING ▲▲▲ happens from BOTTOM-TO-TOP, e.g. the results of `expand-macros` are (eventually) passed to `expand-resolve`
 
@@ -100,9 +103,10 @@
 ;;; +----------------------------------------------------------------------------------------------------+
 
 (defn- save-query-execution!
-  "Save (or update) a `QueryExecution`."
+  "Save a `QueryExecution` and update the average execution time for the corresponding `Query`."
   [query-execution]
   (u/prog1 query-execution
+    (query/update-average-execution-time! (:hash query-execution) (:running_time query-execution))
     (db/insert! QueryExecution (dissoc query-execution :json_query))))
 
 (defn- save-and-return-failed-query!
@@ -126,17 +130,20 @@
 (defn- save-and-return-successful-query!
   "Save QueryExecution state and construct a completed (successful) query response"
   [query-execution query-result]
-  ;; record our query execution and format response
-  (-> (assoc query-execution
-        :running_time (- (System/currentTimeMillis)
-                         (:start_time_millis query-execution))
-        :result_rows  (get query-result :row_count 0))
-      (dissoc :start_time_millis)
-      save-query-execution!
-      ;; at this point we've saved and we just need to massage things into our final response format
-      (dissoc :error :result_rows :hash :executor_id :native :card_id :dashboard_id :pulse_id)
-      (merge query-result)
-      (assoc :status :completed)))
+  (let [query-execution (-> (assoc query-execution
+                              :running_time (- (System/currentTimeMillis)
+                                               (:start_time_millis query-execution))
+                              :result_rows  (get query-result :row_count 0))
+                            (dissoc :start_time_millis))]
+    ;; only insert a new record into QueryExecution if the results *were not* cached (i.e., only if a Query was actually ran)
+    (when-not (:cached query-result)
+      (save-query-execution! query-execution))
+    ;; ok, now return the results in the normal response format
+    (merge (dissoc query-execution :error :result_rows :hash :executor_id :native :card_id :dashboard_id :pulse_id)
+           query-result
+           {:status                 :completed
+            :average_execution_time (when (:cached query-result)
+                                      (query/average-execution-time-ms (:hash query-execution)))})))
 
 
 (defn- assert-query-status-successful
diff --git a/src/metabase/query_processor/expand.clj b/src/metabase/query_processor/expand.clj
index 0ec8e1e48a732c562de56d506f048fc4a611169d..23f660f02db5240fca2b0d47853ddb76de327e35 100644
--- a/src/metabase/query_processor/expand.clj
+++ b/src/metabase/query_processor/expand.clj
@@ -109,7 +109,7 @@
      (relative-datetime -31 :day)"
   ([n]                (s/validate (s/eq :current) (normalize-token n))
                       (relative-datetime 0 nil))
-  ([n :- s/Int, unit] (i/map->RelativeDatetime {:amount n, :unit (if (zero? n)
+  ([n :- s/Int, unit] (i/map->RelativeDatetime {:amount n, :unit (if (nil? unit)
                                                                    :day                        ; give :unit a default value so we can simplify the schema a bit and require a :unit
                                                                    (normalize-token unit))})))
 
@@ -307,7 +307,7 @@
       :next    (recur f  1 unit))
     (let [f (datetime-field f unit)]
       (cond
-        (core/= n  0) (= f (value f (relative-datetime :current)))
+        (core/= n  0) (= f (value f (relative-datetime  0 unit)))
         (core/= n -1) (= f (value f (relative-datetime -1 unit)))
         (core/= n  1) (= f (value f (relative-datetime  1 unit)))
         (core/< n -1) (between f (value f (relative-datetime  n unit))
diff --git a/src/metabase/query_processor/middleware/cache.clj b/src/metabase/query_processor/middleware/cache.clj
new file mode 100644
index 0000000000000000000000000000000000000000..06bea49173d50e81db33bda3737b1238e842219a
--- /dev/null
+++ b/src/metabase/query_processor/middleware/cache.clj
@@ -0,0 +1,146 @@
+(ns metabase.query-processor.middleware.cache
+  "Middleware that returns cached results for queries when applicable.
+
+   If caching is enabled (`enable-query-caching` is `true`) cached results will be returned for Cards if possible. There's
+   a global default TTL defined by the setting `query-caching-default-ttl`, but individual Cards can override this value
+   with custom TTLs with a value for `:cache_ttl`.
+
+   For all other queries, caching is skipped.
+
+   Various caching backends are defined in `metabase.query-processor.middleware.cache-backend` namespaces.
+   The default backend is `db`, which uses the application database; this value can be changed by setting the env var
+   `MB_QP_CACHE_BACKEND`.
+
+    Refer to `metabase.query-processor.middleware.cache-backend.interface` for more details about how the cache backends themselves."
+  (:require [clojure.tools.logging :as log]
+            [metabase.config :as config]
+            [metabase.public-settings :as public-settings]
+            [metabase.query-processor.middleware.cache-backend.interface :as i]
+            [metabase.query-processor.util :as qputil]
+            [metabase.util :as u]))
+
+(def ^:dynamic ^Boolean *ignore-cached-results*
+  "Should we force the query to run, ignoring cached results even if they're available?
+   Setting this to `true` will run the query again and will still save the updated results."
+  false)
+
+
+;;; ------------------------------------------------------------ Backend ------------------------------------------------------------
+
+(def ^:private backend-instance
+  (atom nil))
+
+(defn- valid-backend? [instance] (extends? i/IQueryProcessorCacheBackend (class instance)))
+
+(defn- get-backend-instance-in-namespace
+  "Return a valid query cache backend `instance` in BACKEND-NS-SYMB, or throw an Exception if none exists."
+  ;; if for some reason the resolved var doesn't satisfy `IQueryProcessorCacheBackend` we'll reload the namespace
+  ;; it belongs to and try one more time.
+  ;; This fixes the issue in dev sessions where the interface namespace gets reloaded causing the cache implementation
+  ;; to no longer satisfy the protocol
+  ([backend-ns-symb]
+   (get-backend-instance-in-namespace backend-ns-symb :allow-reload))
+  ([backend-ns-symb allow-reload?]
+   (let [varr (ns-resolve backend-ns-symb 'instance)]
+     (cond
+       (not varr)             (throw (Exception. (str "No var named 'instance' found in namespace " backend-ns-symb)))
+       (valid-backend? @varr) @varr
+       allow-reload?          (do (require backend-ns-symb :reload)
+                                  (get-backend-instance-in-namespace backend-ns-symb false))
+       :else                  (throw (Exception. (format "%s/instance doesn't satisfy IQueryProcessorCacheBackend" backend-ns-symb)))))))
+
+(defn- set-backend!
+  "Set the cache backend to the cache defined by the keyword BACKEND.
+
+   (This should be something like `:db`, `:redis`, or `:memcached`. See the
+   documentation in `metabase.query-processor.middleware.cache-backend.interface` for details on how this works.)"
+  ([]
+   (set-backend! (config/config-kw :mb-qp-cache-backend)))
+  ([backend]
+   (let [backend-ns (symbol (str "metabase.query-processor.middleware.cache-backend." (munge (name backend))))]
+     (require backend-ns)
+     (log/info "Using query processor cache backend:" (u/format-color 'blue backend) (u/emoji "💾"))
+     (reset! backend-instance (get-backend-instance-in-namespace backend-ns)))))
+
+
+
+;;; ------------------------------------------------------------ Cache Operations ------------------------------------------------------------
+
+(defn- cached-results [query-hash max-age-seconds]
+  (when-not *ignore-cached-results*
+    (when-let [results (i/cached-results @backend-instance query-hash max-age-seconds)]
+      (assert (u/is-temporal? (:updated_at results))
+        "cached-results should include an `:updated_at` field containing the date when the query was last ran.")
+      (log/info "Returning cached results for query" (u/emoji "💾"))
+      (assoc results :cached true))))
+
+(defn- save-results!  [query-hash results]
+  (log/info "Caching results for next time for query" (u/emoji "💾"))
+  (i/save-results! @backend-instance query-hash results))
+
+
+;;; ------------------------------------------------------------ Middleware ------------------------------------------------------------
+
+(defn- is-cacheable? ^Boolean [{cache-ttl :cache_ttl}]
+  (boolean (and (public-settings/enable-query-caching)
+                cache-ttl)))
+
+(defn- results-are-below-max-byte-threshold?
+  "Measure the size of the `:rows` in QUERY-RESULTS and see whether they're smaller than `query-caching-max-kb`
+   *before* compression."
+  ^Boolean [{{rows :rows} :data}]
+  (let [max-bytes (* (public-settings/query-caching-max-kb) 1024)]
+    ;; We don't want to serialize the entire result set since that could explode if the query is one that returns a
+    ;; huge number of rows. (We also want to keep `:rows` lazy.)
+    ;; So we'll serialize one row at a time, and keep a running total of bytes; if we pass the `query-caching-max-kb`
+    ;; threshold, we'll fail right away.
+    (loop [total-bytes 0, [row & more] rows]
+      (cond
+        (> total-bytes max-bytes) false
+        (not row)                 true
+        :else                     (recur (+ total-bytes (count (str row)))
+                                         more)))))
+
+(defn- save-results-if-successful! [query-hash results]
+  (when (and (= (:status results) :completed)
+             (or (results-are-below-max-byte-threshold? results)
+                 (log/info "Results are too large to cache." (u/emoji "😫"))))
+    (save-results! query-hash results)))
+
+(defn- run-query-and-save-results-if-successful! [query-hash qp query]
+  (let [start-time-ms (System/currentTimeMillis)
+        results       (qp query)
+        total-time-ms (- (System/currentTimeMillis) start-time-ms)
+        min-ttl-ms    (* (public-settings/query-caching-min-ttl) 1000)]
+    (log/info (format "Query took %d ms to run; miminum for cache eligibility is %d ms" total-time-ms min-ttl-ms))
+    (when (>= total-time-ms min-ttl-ms)
+      (save-results-if-successful! query-hash results))
+    results))
+
+(defn- run-query-with-cache [qp {cache-ttl :cache_ttl, :as query}]
+  (let [query-hash (qputil/query-hash query)]
+    (or (cached-results query-hash cache-ttl)
+        (run-query-and-save-results-if-successful! query-hash qp query))))
+
+
+(defn maybe-return-cached-results
+  "Middleware for caching results of a query if applicable.
+   In order for a query to be eligible for caching:
+
+     *  Caching (the `enable-query-caching` Setting) must be enabled
+     *  The query must pass a `:cache_ttl` value. For Cards, this can be the value of `:cache_ttl`,
+        otherwise falling back to the value of the `query-caching-default-ttl` Setting.
+     *  The query must already be permissions-checked. Since the cache bypasses the normal
+        query processor pipeline, the ad-hoc permissions-checking middleware isn't applied for cached results.
+        (The various `/api/card/` endpoints that make use of caching do `can-read?` checks for the Card *before*
+        running the query, satisfying this requirement.)
+     *  The result *rows* of the query must be less than `query-caching-max-kb` when serialized (before compression)."
+  [qp]
+  ;; choose the caching backend if needed
+  (when-not @backend-instance
+    (set-backend!))
+  ;; ok, now do the normal middleware thing
+  (fn [query]
+    (if-not (is-cacheable? query)
+      (qp query)
+      (run-query-with-cache qp query))))
diff --git a/src/metabase/query_processor/middleware/cache_backend/db.clj b/src/metabase/query_processor/middleware/cache_backend/db.clj
new file mode 100644
index 0000000000000000000000000000000000000000..4964d8a88a83ad027b33f3781bbc0e965e568585
--- /dev/null
+++ b/src/metabase/query_processor/middleware/cache_backend/db.clj
@@ -0,0 +1,42 @@
+(ns metabase.query-processor.middleware.cache-backend.db
+  (:require [toucan.db :as db]
+            (metabase.models [interface :as models]
+                             [query-cache :refer [QueryCache]])
+            [metabase.public-settings :as public-settings]
+            [metabase.query-processor.middleware.cache-backend.interface :as i]
+            [metabase.util :as u]))
+
+(defn- cached-results
+  "Return cached results for QUERY-HASH if they exist and are newer than MAX-AGE-SECONDS."
+  [query-hash max-age-seconds]
+  (when-let [{:keys [results updated_at]} (db/select-one [QueryCache :results :updated_at]
+                                            :query_hash query-hash
+                                            :updated_at [:>= (u/->Timestamp (- (System/currentTimeMillis)
+                                                                               (* 1000 max-age-seconds)))])]
+    (assoc results :updated_at updated_at)))
+
+(defn- purge-old-cache-entries!
+  "Delete any cache entries that are older than the global max age `max-cache-entry-age-seconds` (currently 3 months)."
+  []
+  (db/simple-delete! QueryCache
+    :updated_at [:<= (u/->Timestamp (- (System/currentTimeMillis)
+                                       (* 1000 (public-settings/query-caching-max-ttl))))]))
+
+(defn- save-results!
+  "Save the RESULTS of query with QUERY-HASH, updating an existing QueryCache entry
+  if one already exists, otherwise creating a new entry."
+  [query-hash results]
+  (purge-old-cache-entries!)
+  (or (db/update-where! QueryCache {:query_hash query-hash}
+        :updated_at (u/new-sql-timestamp)
+        :results    (models/compress results)) ; have to manually call these here since Toucan doesn't call type conversion fns for update-where! (yet)
+      (db/insert! QueryCache
+        :query_hash query-hash
+        :results    results))
+  :ok)
+
+(def instance
+  "Implementation of `IQueryProcessorCacheBackend` that uses the database for caching results."
+  (reify i/IQueryProcessorCacheBackend
+    (cached-results [_ query-hash max-age-seconds] (cached-results query-hash max-age-seconds))
+    (save-results!  [_ query-hash results]         (save-results! query-hash results))))
diff --git a/src/metabase/query_processor/middleware/cache_backend/interface.clj b/src/metabase/query_processor/middleware/cache_backend/interface.clj
new file mode 100644
index 0000000000000000000000000000000000000000..4dbf762ca981c6f32ee4cc28fa0d0995f5be2c46
--- /dev/null
+++ b/src/metabase/query_processor/middleware/cache_backend/interface.clj
@@ -0,0 +1,37 @@
+(ns metabase.query-processor.middleware.cache-backend.interface
+  "Interface used to define different Query Processor cache backends.
+   Defining a backend is straightforward: define a new namespace with the pattern
+
+     metabase.query-processor.middleware.cache-backend.<backend>
+
+   Where backend is a key representing the backend, e.g. `db`, `redis`, or `memcached`.
+
+   In that namespace, create an object that reifies (or otherwise implements) `IQueryProcessorCacheBackend`.
+   This object *must* be stored in a var called `instance`.
+
+   That's it. See `metabase.query-processor.middleware.cache-backend.db` for a complete example of how this is done.")
+
+
+(defprotocol IQueryProcessorCacheBackend
+  "Protocol that different Metabase cache backends must implement.
+
+   QUERY-HASH as passed below is a byte-array representing a 256-byte SHA3 hash; encode this as needed for use as a
+   cache entry key. RESULTS are passed (and should be returned) as a Clojure object, and individual backends are free
+   to encode this as appropriate when storing the results. (It's probably not a bad idea to compress the results; this
+   is what the `:db` backend does.)"
+
+  (cached-results [this, query-hash, ^Integer max-age-seconds]
+    "Return cached results for the query with byte array QUERY-HASH if those results are present in the cache and are less
+     than MAX-AGE-SECONDS old. Otherwise, return `nil`.
+
+  This method must also return a Timestamp from when the query was last ran. This must be `assoc`ed with the query results
+  under the key `:updated_at`.
+
+    (cached-results [_ query-hash max-age-seconds]
+      (when-let [[results updated-at] (maybe-fetch-results query-hash max-age-seconds)]
+        (assoc results :updated_at updated-at)))")
+
+  (save-results! [this query-hash results]
+    "Add a cache entry with the RESULTS of running query with byte array QUERY-HASH.
+     This should replace any prior entries for QUERY-HASH and update the cache timestamp to the current system time.
+     (This is also an appropriate point to purge any entries older than the value of the `query-caching-max-ttl` Setting.)"))
diff --git a/src/metabase/query_processor/parameters.clj b/src/metabase/query_processor/parameters.clj
index af2793c722d002bb37962d3773ac7966516f9e64..d3ac5416c555c4f1ef0f06ce0451bba6d7a177ce 100644
--- a/src/metabase/query_processor/parameters.clj
+++ b/src/metabase/query_processor/parameters.clj
@@ -10,18 +10,39 @@
             [metabase.util :as u])
   (:import (org.joda.time DateTimeConstants DateTime)))
 
+;;; +-------------------------------------------------------------------------------------------------------+
+;;; |                                    DATE RANGES & PERIODS                                              |
+;;; +-------------------------------------------------------------------------------------------------------+
+
+;; Both in MBQL and SQL parameter substitution a field value is compared to a date range, either relative or absolute.
+;; Currently the field value is casted to a day (ignoring the time of day), so the ranges should have the same
+;; granularity level.
+;;
+;; See https://github.com/metabase/metabase/pull/4607#issuecomment-290884313 how we could support
+;; hour/minute granularity in field parameter queries.
+
+
+(defn- day-range
+  [^DateTime start, ^DateTime end]
+  {:end   end
+   :start start})
+
+(defn- week-range
+  [^DateTime start, ^DateTime end]
+    ;; weeks always start on SUNDAY and end on SATURDAY
+    ;; NOTE: in Joda the week starts on Monday and ends on Sunday, so to get the right Sunday we rollback 1 week
+   {:end   (.withDayOfWeek end DateTimeConstants/SATURDAY)
+    :start (.withDayOfWeek ^DateTime (t/minus start (t/weeks 1)) DateTimeConstants/SUNDAY)})
+
+(defn- month-range
+  [^DateTime start, ^DateTime end]
+  {:end   (t/last-day-of-the-month end)
+   :start (t/first-day-of-the-month start)})
 
-(def ^:private ^:const relative-dates
-  #{"today"
-    "yesterday"
-    "past7days"
-    "past30days"
-    "thisweek"
-    "thismonth"
-    "thisyear"
-    "lastweek"
-    "lastmonth"
-    "lastyear"})
+(defn- year-range
+  [^DateTime start, ^DateTime end]
+  {:end   (t/last-day-of-the-month  (.withMonthOfYear end DateTimeConstants/DECEMBER))
+   :start (t/first-day-of-the-month (.withMonthOfYear start DateTimeConstants/JANUARY))})
 
 (defn- start-of-quarter [quarter year]
   (t/first-day-of-the-month (.withMonthOfYear (t/date-time year) (case quarter
@@ -29,84 +50,179 @@
                                                                    "Q2" DateTimeConstants/APRIL
                                                                    "Q3" DateTimeConstants/JULY
                                                                    "Q4" DateTimeConstants/OCTOBER))))
+(defn- quarter-range
+  [quarter year]
+  (let [dt (start-of-quarter quarter year)]
+    {:end   (t/last-day-of-the-month (t/plus dt (t/months 2)))
+     :start (t/first-day-of-the-month dt)}))
+
+(def ^:private operations-by-date-unit
+  {"day"   {:unit-range day-range
+            :to-period  t/days}
+   "week"  {:unit-range week-range
+            :to-period  t/weeks}
+   "month" {:unit-range month-range
+            :to-period  t/months}
+   "year"  {:unit-range year-range
+            :to-period  t/years}})
+
+(defn- parse-absolute-date
+  [date]
+  (tf/parse (tf/formatters :date-opt-time) date))
+
+;;; +-------------------------------------------------------------------------------------------------------+
+;;; |                                    DATE STRING DECODERS                                               |
+;;; +-------------------------------------------------------------------------------------------------------+
+
+;; For parsing date strings and producing either a date range (for raw SQL parameter substitution) or a MBQL clause
+
+(defn- expand-parser-groups
+  [group-label group-value]
+  (case group-label
+    :unit (conj (seq (get operations-by-date-unit group-value))
+                [group-label group-value])
+    :int-value [[group-label (Integer/parseInt group-value)]]
+    (:date :date-1 :date-2) [[group-label (parse-absolute-date group-value)]]
+    [[group-label group-value]]))
 
-(defn- week-range [^DateTime dt]
-  ;; weeks always start on SUNDAY and end on SATURDAY
-  ;; NOTE: in Joda the week starts on Monday and ends on Sunday, so to get the right Sunday we rollback 1 week
-  {:end   (.withDayOfWeek dt DateTimeConstants/SATURDAY)
-   :start (.withDayOfWeek ^DateTime (t/minus dt (t/weeks 1)) DateTimeConstants/SUNDAY)})
-
-(defn- month-range [^DateTime dt]
-  {:end   (t/last-day-of-the-month dt)
-   :start (t/first-day-of-the-month dt)})
-
-;; NOTE: this is perhaps a little hacky, but we are assuming that `dt` will be in the first month of the quarter
-(defn- quarter-range [^DateTime dt]
-  {:end   (t/last-day-of-the-month (t/plus dt (t/months 2)))
-   :start (t/first-day-of-the-month dt)})
-
-(defn- year-range [^DateTime dt]
-  {:end   (t/last-day-of-the-month  (.withMonthOfYear dt DateTimeConstants/DECEMBER))
-   :start (t/first-day-of-the-month (.withMonthOfYear dt DateTimeConstants/JANUARY))})
-
-(defn- absolute-date->range
-  "Take a given string description of an absolute date range and return a MAP with a given `:start` and `:end`.
-
-   Supported formats:
-
-      \"2014-05-10~2014-05-16\"
-      \"Q1-2016\"
-      \"2016-04\"
-      \"2016-04-12\""
-  [value]
-  (if (s/includes? value "~")
-    ;; these values are already expected to be iso8601 strings, so we are done
-    (zipmap [:start :end] (s/split value #"~" 2))
-    ;; these cases represent fixed date ranges, but we need to calculate start/end still
-    (->> (cond
-           ;; quarter-year (Q1-2016)
-           (s/starts-with? value "Q") (let [[quarter year] (s/split value #"-" 2)]
-                                        (quarter-range (start-of-quarter quarter (Integer/parseInt year))))
-           ;; year-month (2016-04)
-           (= (count value) 7)        (month-range (tf/parse (tf/formatters :year-month) value))
-           ;; default is to assume a single day (2016-04-18).  we still parse just to validate.
-           :else                      (let [dt (tf/parse (tf/formatters :year-month-day) value)]
-                                        {:start dt, :end dt}))
-         (m/map-vals (partial tf/unparse (tf/formatters :year-month-day))))))
-
-
-(defn- relative-date->range
-  "Take a given string description of a relative date range such as 'lastmonth' and return a MAP with a given
-   `:start` and `:end` as iso8601 string formatted dates.  Values should be appropriate for the given REPORT-TIMEZONE."
-  [value report-timezone]
-  (let [tz        (t/time-zone-for-id report-timezone)
-        formatter (tf/formatter "yyyy-MM-dd" tz)
-        today     (.withTimeAtStartOfDay (t/to-time-zone (t/now) tz))]
-    (->> (case value
-           "past7days"  {:end   (t/minus today (t/days 1))
-                         :start (t/minus today (t/days 7))}
-           "past30days" {:end   (t/minus today (t/days 1))
-                         :start (t/minus today (t/days 30))}
-           "thisweek"   (week-range today)
-           "thismonth"  (month-range today)
-           "thisyear"   (year-range today)
-           "lastweek"   (week-range (t/minus today (t/weeks 1)))
-           "lastmonth"  (month-range (t/minus today (t/months 1)))
-           "lastyear"   (year-range (t/minus today (t/years 1)))
-           "yesterday"  {:end   (t/minus today (t/days 1))
-                         :start (t/minus today (t/days 1))}
-           "today"      {:end   today
-                         :start today})
-         ;; the above values are JodaTime objects, so unparse them to iso8601 strings
-         (m/map-vals (partial tf/unparse formatter)))))
-
-(defn date->range
-  "Convert a relative or absolute date range VALUE to a map with `:start` and `:end` keys."
-  [value report-timezone]
-  (if (contains? relative-dates value)
-    (relative-date->range value report-timezone)
-    (absolute-date->range value)))
+(defn- regex->parser
+  "Takes a regex and labels matching the regex capturing groups. Returns a parser which
+  takes a parameter value, validates the value against regex and gives a map of labels
+  and group values. Respects the following special label names:
+      :unit – finds a matching date unit and merges date unit operations to the result
+      :int-value – converts the group value to integer
+      :date, :date1, date2 – converts the group value to absolute date"
+  [regex group-labels]
+  (fn [param-value]
+    (when-let [regex-result (re-matches regex param-value)]
+      (into {} (mapcat expand-parser-groups group-labels (rest regex-result))))))
 
+;; Decorders consist of:
+;; 1) Parser which tries to parse the date parameter string
+;; 2) Range decoder which takes the parser output and produces a date range relative to the given datetime
+;; 3) Filter decoder which takes the parser output and produces a mbql clause for a given mbql field reference
+
+(def ^:private relative-date-string-decoders
+  [{:parser #(= % "today")
+    :range  (fn [_ dt]
+              {:start dt,
+               :end   dt})
+    :filter (fn [_ field] ["=" field ["relative_datetime" "current"]])}
+
+   {:parser #(= % "yesterday")
+    :range  (fn [_ dt]
+              {:start (t/minus dt (t/days 1))
+               :end   (t/minus dt (t/days 1))})
+    :filter (fn [_ field] ["=" field ["relative_datetime" -1 "day"]])}
+
+   {:parser (regex->parser #"past([0-9]+)(day|week|month|year)s", [:int-value :unit])
+    :range  (fn [{:keys [unit int-value unit-range to-period]} dt]
+              (unit-range (t/minus dt (to-period int-value))
+                          (t/minus dt (to-period 1))))
+    :filter (fn [{:keys [unit int-value]} field]
+              ["TIME_INTERVAL" field (- int-value) unit])}
+
+   {:parser (regex->parser #"next([0-9]+)(day|week|month|year)s" [:int-value :unit])
+    :range  (fn [{:keys [unit int-value unit-range to-period]} dt]
+              (unit-range (t/plus dt (to-period 1))
+                          (t/plus dt (to-period int-value))))
+    :filter (fn [{:keys [unit int-value]} field]
+              ["TIME_INTERVAL" field int-value unit])}
+
+   {:parser (regex->parser #"last(day|week|month|year)" [:unit])
+    :range  (fn [{:keys [unit-range to-period]} dt]
+              (let [last-unit (t/minus dt (to-period 1))]
+                (unit-range last-unit last-unit)))
+    :filter (fn [{:keys [unit]} field]
+              ["TIME_INTERVAL" field "last" unit])}
+
+   {:parser (regex->parser #"this(day|week|month|year)" [:unit])
+    :range  (fn [{:keys [unit-range]} dt]
+              (unit-range dt dt))
+    :filter (fn [{:keys [unit]} field]
+              ["TIME_INTERVAL" field "current" unit])}])
+
+(defn- day->iso8601 [date]
+  (tf/unparse (tf/formatters :year-month-day) date))
+
+(defn- range->filter
+  [{:keys [start end]} field]
+  ["BETWEEN" field (day->iso8601 start) (day->iso8601 end)])
+
+(def ^:private absolute-date-string-decoders
+  ;; year and month
+  [{:parser (regex->parser #"([0-9]{4}-[0-9]{2})" [:date])
+    :range  (fn [{:keys [date]} _]
+              (month-range date date))
+    :filter (fn [{:keys [date]} field]
+              (range->filter (month-range date date) field))}
+   ;; quarter year
+   {:parser (regex->parser #"(Q[1-4]{1})-([0-9]{4})" [:quarter :year])
+    :range  (fn [{:keys [quarter year]} _]
+              (quarter-range quarter (Integer/parseInt year)))
+    :filter (fn [{:keys [quarter year]} field]
+              (range->filter (quarter-range quarter (Integer/parseInt year))
+                             field))}
+   ;; single day
+   {:parser (regex->parser #"([0-9-T:]+)" [:date])
+    :range  (fn [{:keys [date]} _]
+              {:start date, :end date})
+    :filter (fn [{:keys [date]} field]
+              (let [iso8601date (day->iso8601 date)]
+                ["BETWEEN" field iso8601date iso8601date]))}
+   ;; day range
+   {:parser (regex->parser #"([0-9-T:]+)~([0-9-T:]+)" [:date-1 :date-2])
+    :range  (fn [{:keys [date-1 date-2]} _]
+              {:start date-1, :end date-2})
+    :filter (fn [{:keys [date-1 date-2]} field]
+              ["BETWEEN" field (day->iso8601 date-1) (day->iso8601 date-2)])}
+   ;; before day
+   {:parser (regex->parser #"~([0-9-T:]+)" [:date])
+    :range  (fn [{:keys [date]} _]
+              {:end date})
+    :filter (fn [{:keys [date]} field]
+              ["<" field (day->iso8601 date)])}
+   ;; after day
+   {:parser (regex->parser #"([0-9-T:]+)~" [:date])
+    :range  (fn [{:keys [date]} _]
+              {:start date})
+    :filter (fn [{:keys [date]} field]
+              [">" field (day->iso8601 date)])}])
+
+(def ^:private all-date-string-decoders
+  (concat relative-date-string-decoders absolute-date-string-decoders))
+
+(defn- execute-decoders
+  "Returns the first successfully decoded value, run through both
+   parser and a range/filter decoder depending on `decoder-type`."
+  [decoders decoder-type decoder-param date-string]
+  (some (fn [{parser :parser, parser-result-decoder decoder-type}]
+          (when-let [parser-result (parser date-string)]
+            (parser-result-decoder parser-result decoder-param)))
+        decoders))
+
+(defn date-string->range
+  "Takes a string description of a date range such as 'lastmonth' or '2016-07-15~2016-08-6' and
+   return a MAP with `:start` and `:end` as iso8601 string formatted dates, respecting the given timezone."
+  [date-string report-timezone]
+  (let [tz (t/time-zone-for-id report-timezone)
+        formatter-local-tz (tf/formatter "yyyy-MM-dd" tz)
+        formatter-no-tz (tf/formatter "yyyy-MM-dd")
+        today (.withTimeAtStartOfDay (t/to-time-zone (t/now) tz))]
+    ;; Relative dates respect the given time zone because a notion like "last 7 days" might mean a different range of days
+    ;; depending on the user timezone
+    (or (->> (execute-decoders relative-date-string-decoders :range today date-string)
+             (m/map-vals (partial tf/unparse formatter-local-tz)))
+        ;; Absolute date ranges don't need the time zone conversion because in SQL the date ranges are compared against
+        ;; the db field value that is casted granularity level of a day in the db time zone
+        (->> (execute-decoders absolute-date-string-decoders :range nil date-string)
+             (m/map-vals (partial tf/unparse formatter-no-tz))))))
+
+(defn- date-string->filter
+  "Takes a string description of a date range such as 'lastmonth' or '2016-07-15~2016-08-6' and returns a
+   corresponding MBQL filter clause for a given field reference."
+  [date-string field-reference]
+  (execute-decoders all-date-string-decoders :filter field-reference date-string))
 
 ;;; +-------------------------------------------------------------------------------------------------------+
 ;;; |                                             MBQL QUERIES                                              |
@@ -125,27 +241,13 @@
     ;; otherwise convert to a Long
     :else                                   (Long/parseLong param-value)))
 
-
 (defn- build-filter-clause [{param-type :type, param-value :value, [_ field] :target}]
   (let [param-value (parse-param-value-for-type param-type param-value)]
     (cond
       ;; default behavior (non-date filtering) is to use a simple equals filter
       (not (s/starts-with? param-type "date")) ["=" field param-value]
-      ;; relative date range
-      (contains? relative-dates param-value)   (case param-value
-                                                 "past7days"  ["TIME_INTERVAL" field -7 "day"]
-                                                 "past30days" ["TIME_INTERVAL" field -30 "day"]
-                                                 "thisweek"   ["TIME_INTERVAL" field "current" "week"]
-                                                 "thismonth"  ["TIME_INTERVAL" field "current" "month"]
-                                                 "thisyear"   ["TIME_INTERVAL" field "current" "year"]
-                                                 "lastweek"   ["TIME_INTERVAL" field "last" "week"]
-                                                 "lastmonth"  ["TIME_INTERVAL" field "last" "month"]
-                                                 "lastyear"   ["TIME_INTERVAL" field "last" "year"]
-                                                 "yesterday"  ["=" field ["relative_datetime" -1 "day"]]
-                                                 "today"      ["=" field ["relative_datetime" "current"]])
-      ;; absolute date range
-      :else                                    (let [{:keys [start end]} (absolute-date->range param-value)]
-                                                 ["BETWEEN" field start end]))))
+      ;; date range
+      :else (date-string->filter param-value field))))
 
 (defn- merge-filter-clauses [base addtl]
   (cond
diff --git a/src/metabase/query_processor/sql_parameters.clj b/src/metabase/query_processor/sql_parameters.clj
index 1ebe0d74917d9aebd261e12d241019119fad350f..a48cb7b71d810e10af732b03f382f4a5c1462b52 100644
--- a/src/metabase/query_processor/sql_parameters.clj
+++ b/src/metabase/query_processor/sql_parameters.clj
@@ -58,13 +58,14 @@
 ;; TAGS in this case are simple params like {{x}} that get replaced with a single value ("ABC" or 1) as opposed to a "FieldFilter" clause like Dimensions
 (def ^:private TagParam
   "Schema for values passed in as part of the `:template_tags` list."
-  {(s/optional-key :id)        su/NonBlankString ; this is used internally by the frontend
-   :name                       su/NonBlankString
-   :display_name               su/NonBlankString
-   :type                       (s/enum "number" "dimension" "text" "date")
-   (s/optional-key :dimension) [s/Any]
-   (s/optional-key :required)  s/Bool
-   (s/optional-key :default)   s/Any})
+  {(s/optional-key :id)          su/NonBlankString ; this is used internally by the frontend
+   :name                         su/NonBlankString
+   :display_name                 su/NonBlankString
+   :type                         (s/enum "number" "dimension" "text" "date")
+   (s/optional-key :dimension)   [s/Any]
+   (s/optional-key :widget_type) su/NonBlankString
+   (s/optional-key :required)    s/Bool
+   (s/optional-key :default)     s/Any})
 
 (def ^:private DimensionValue
   {:type                   su/NonBlankString
@@ -198,13 +199,13 @@
       (->replacement-snippet-info \"ABC\") -> {:replacement-snippet \"?\", :prepared-statement-args \"ABC\"}"))
 
 
-(defn- relative-date-param-type? [param-type] (contains? #{"date/range" "date/month-year" "date/quarter-year" "date/relative"} param-type))
+(defn- relative-date-param-type? [param-type] (contains? #{"date/range" "date/month-year" "date/quarter-year" "date/relative" "date/all-options"} param-type))
 (defn- date-param-type?          [param-type] (str/starts-with? param-type "date/"))
 
 ;; for relative dates convert the param to a `DateRange` record type and call `->replacement-snippet-info` on it
 (s/defn ^:private ^:always-validate relative-date-dimension-value->replacement-snippet-info :- ParamSnippetInfo
   [value]
-  (->replacement-snippet-info (map->DateRange ((resolve 'metabase.query-processor.parameters/date->range) value *timezone*)))) ; TODO - get timezone from query dict
+  (->replacement-snippet-info (map->DateRange ((resolve 'metabase.query-processor.parameters/date-string->range) value *timezone*)))) ; TODO - get timezone from query dict
 
 (s/defn ^:private ^:always-validate dimension-value->equals-clause-sql :- ParamSnippetInfo
   [value]
@@ -242,13 +243,13 @@
    :prepared-statement-args (reduce concat (map :prepared-statement-args replacement-snippet-maps))})
 
 (extend-protocol ISQLParamSubstituion
-  nil         (->replacement-snippet-info [this] (honeysql->replacement-snippet-info this))
-  Object      (->replacement-snippet-info [this] (honeysql->replacement-snippet-info (str this)))
-  Number      (->replacement-snippet-info [this] (honeysql->replacement-snippet-info this))
-  Boolean     (->replacement-snippet-info [this] (honeysql->replacement-snippet-info this))
-  Keyword     (->replacement-snippet-info [this] (honeysql->replacement-snippet-info this))
-  SqlCall     (->replacement-snippet-info [this] (honeysql->replacement-snippet-info this))
-  NoValue     (->replacement-snippet-info [_]    {:replacement-snippet ""})
+  nil     (->replacement-snippet-info [this] (honeysql->replacement-snippet-info this))
+  Object  (->replacement-snippet-info [this] (honeysql->replacement-snippet-info (str this)))
+  Number  (->replacement-snippet-info [this] (honeysql->replacement-snippet-info this))
+  Boolean (->replacement-snippet-info [this] (honeysql->replacement-snippet-info this))
+  Keyword (->replacement-snippet-info [this] (honeysql->replacement-snippet-info this))
+  SqlCall (->replacement-snippet-info [this] (honeysql->replacement-snippet-info this))
+  NoValue (->replacement-snippet-info [_]    {:replacement-snippet ""})
 
   Date
   (->replacement-snippet-info [{:keys [s]}]
@@ -256,9 +257,11 @@
 
   DateRange
   (->replacement-snippet-info [{:keys [start end]}]
-    (if (= start end)
-      {:replacement-snippet "= ?",             :prepared-statement-args [(u/->Timestamp start)]}
-      {:replacement-snippet "BETWEEN ? AND ?", :prepared-statement-args [(u/->Timestamp start) (u/->Timestamp end)]}))
+    (cond
+      (= start end) {:replacement-snippet "= ?",             :prepared-statement-args [(u/->Timestamp start)]}
+      (nil? start)  {:replacement-snippet "< ?",             :prepared-statement-args [(u/->Timestamp end)]}
+      (nil? end)    {:replacement-snippet "> ?",             :prepared-statement-args [(u/->Timestamp start)]}
+      :else         {:replacement-snippet "BETWEEN ? AND ?", :prepared-statement-args [(u/->Timestamp start) (u/->Timestamp end)]}))
 
   ;; TODO - clean this up if possible!
   Dimension
@@ -342,19 +345,25 @@
 (s/defn ^:private ^:always-validate handle-optional-snippet :- ParamSnippetInfo
   "Create the approprate `:replacement-snippet` for PARAM, combining the value of REPLACEMENT-SNIPPET from the Param->SQL Substitution phase
    with the OPTIONAL-SNIPPET, if any."
-  [{:keys [variable-snippet optional-snippet replacement-snippet], :as snippet-info} :- ParamSnippetInfo]
+  [{:keys [variable-snippet optional-snippet replacement-snippet prepared-statement-args], :as snippet-info} :- ParamSnippetInfo]
   (assoc snippet-info
-    :replacement-snippet (cond
-                           (not optional-snippet)    replacement-snippet                                                 ; if there is no optional-snippet return replacement as-is
-                           (seq replacement-snippet) (str/replace optional-snippet variable-snippet replacement-snippet) ; if replacement-snippet is non blank splice into optional-snippet
-                           :else                     "")))                                                               ; otherwise return blank replacement (i.e. for NoValue)
+    :replacement-snippet     (cond
+                               (not optional-snippet)    replacement-snippet                                                 ; if there is no optional-snippet return replacement as-is
+                               (seq replacement-snippet) (str/replace optional-snippet variable-snippet replacement-snippet) ; if replacement-snippet is non blank splice into optional-snippet
+                               :else                     "")                                                                 ; otherwise return blank replacement (i.e. for NoValue)
+    ;; for every thime the `variable-snippet` occurs in the `optional-snippet` we need to supply an additional set of `prepared-statment-args`
+    ;; e.g. [[ AND ID = {{id}} OR USER_ID = {{id}} ]] should have *2* sets of the prepared statement args for {{id}} since it occurs twice
+    :prepared-statement-args (if-let [occurances (u/occurances-of-substring optional-snippet variable-snippet)]
+                               (apply concat (repeat occurances prepared-statement-args))
+                               prepared-statement-args)))
 
 (s/defn ^:private ^:always-validate add-replacement-snippet-info :- [ParamSnippetInfo]
   "Add `:replacement-snippet` and `:prepared-statement-args` info to the maps in PARAMS-SNIPPETS-INFO by looking at PARAM-KEY->VALUE
    and using the Param->SQL substituion functions."
   [params-snippets-info :- [ParamSnippetInfo], param-key->value :- ParamValues]
   (for [snippet-info params-snippets-info]
-    (handle-optional-snippet (merge snippet-info (s/validate ParamSnippetInfo (->replacement-snippet-info (snippet-value snippet-info param-key->value)))))))
+    (handle-optional-snippet (merge snippet-info
+                                    (s/validate ParamSnippetInfo (->replacement-snippet-info (snippet-value snippet-info param-key->value)))))))
 
 
 
diff --git a/src/metabase/query_processor/util.clj b/src/metabase/query_processor/util.clj
index 3846fb546662a6f6e15dd7d0f4ac8dcb01127db0..dec320e4dc655fec44f52390c155f7996624686f 100644
--- a/src/metabase/query_processor/util.clj
+++ b/src/metabase/query_processor/util.clj
@@ -3,8 +3,7 @@
   (:require (buddy.core [codecs :as codecs]
                         [hash :as hash])
             [cheshire.core :as json]
-            [toucan.db :as db]
-            [metabase.models.query-execution :refer [QueryExecution]]))
+            [toucan.db :as db]))
 
 (defn mbql-query?
   "Is the given query an MBQL query?"
@@ -41,7 +40,7 @@
    (This is done so irrelevant info or options that don't affect query results doesn't result in the same query producing different hashes.)"
   [query]
   {:pre [(map? query)]}
-  (let [{:keys [constraints parameters], :as query} (select-keys query [:database :type :query :parameters :constraints])]
+  (let [{:keys [constraints parameters], :as query} (select-keys query [:database :type :query :native :parameters :constraints])]
     (cond-> query
       (empty? constraints) (dissoc :constraints)
       (empty? parameters)  (dissoc :parameters))))
@@ -50,17 +49,3 @@
   "Return a 256-bit SHA3 hash of QUERY as a key for the cache. (This is returned as a byte array.)"
   [query]
   (hash/sha3-256 (json/generate-string (select-keys-for-hashing query))))
-
-
-;;; ------------------------------------------------------------ Historic Duration Info ------------------------------------------------------------
-
-(defn query-average-duration
-  "Return the average running time of QUERY over the last 10 executions in milliseconds.
-   Returns `nil` if there's not available data."
-  ^Float [query]
-  (when-let [running-times (db/select-field :running_time QueryExecution
-                             :hash (query-hash query)
-                             {:order-by [[:started_at :desc]]
-                              :limit    10})]
-    (float (/ (reduce + running-times)
-              (count running-times)))))
diff --git a/src/metabase/routes.clj b/src/metabase/routes.clj
index f1008afd248e117b8cc67c74f3ef4b838b456403..5bca43914c31490ed907fa9a3cb8607b26557da9 100644
--- a/src/metabase/routes.clj
+++ b/src/metabase/routes.clj
@@ -6,13 +6,14 @@
             [ring.util.response :as resp]
             [stencil.core :as stencil]
             [metabase.api.routes :as api]
+            [metabase.core.initialization-status :as init-status]
             [metabase.public-settings :as public-settings]
             [metabase.util :as u]
             [metabase.util.embed :as embed]))
 
 
 (defn- entrypoint [entry embeddable? {:keys [uri]}]
-  (-> (if ((resolve 'metabase.core/initialized?))
+  (-> (if (init-status/complete?)
         (stencil/render-string (slurp (or (io/resource (str "frontend_client/" entry ".html"))
                                           (throw (Exception. (str "Cannot find './resources/frontend_client/" entry ".html'. Did you remember to build the Metabase frontend?")))))
                                {:bootstrap_json (json/generate-string (public-settings/public-settings))
@@ -40,9 +41,17 @@
   ;; ^/$ -> index.html
   (GET "/" [] index)
   (GET "/favicon.ico" [] (resp/resource-response "frontend_client/favicon.ico"))
-  ;; ^/api/ -> API routes
-  (context "/api" [] api/routes)
-  ; ^/app/ -> static files under frontend_client/app
+  ;; ^/api/health -> Health Check Endpoint
+  (GET "/api/health" [] (if (init-status/complete?)
+                          {:status 200, :body {:status "ok"}}
+                          {:status 503, :body {:status "initializing", :progress (init-status/progress)}}))
+  ;; ^/api/ -> All other API routes
+  (context "/api" [] (fn [& args]
+                       ;; if Metabase is not finished initializing, return a generic error message rather than something potentially confusing like "DB is not set up"
+                       (if-not (init-status/complete?)
+                         {:status 503, :body "Metabase is still initializing. Please sit tight..."}
+                         (apply api/routes args))))
+  ;; ^/app/ -> static files under frontend_client/app
   (context "/app" []
     (route/resources "/" {:root "frontend_client/app"})
     ;; return 404 for anything else starting with ^/app/ that doesn't exist
diff --git a/src/metabase/sync_database/analyze.clj b/src/metabase/sync_database/analyze.clj
index 35cee5ee35e9565ecc50d425d62e49982cc532b9..bad37a0c7a53b7e267668e923e7fc08daa921f1b 100644
--- a/src/metabase/sync_database/analyze.clj
+++ b/src/metabase/sync_database/analyze.clj
@@ -14,21 +14,23 @@
             [metabase.sync-database.interface :as i]
             [metabase.util :as u]))
 
-(def ^:private ^:const percent-valid-url-threshold
+(def ^:private ^:const ^Float percent-valid-url-threshold
   "Fields that have at least this percent of values that are valid URLs should be given a special type of `:type/URL`."
   0.95)
 
-
-(def ^:private ^:const low-cardinality-threshold
+(def ^:private ^:const ^Integer low-cardinality-threshold
   "Fields with less than this many distinct values should automatically be given a special type of `:type/Category`."
   300)
 
-(def ^:private ^:const field-values-entry-max-length
+(def ^:private ^:const ^Integer field-values-entry-max-length
   "The maximum character length for a stored `FieldValues` entry."
   100)
 
+(def ^:private ^:const ^Integer field-values-total-max-length
+  "Maximum total length for a FieldValues entry (combined length of all values for the field)."
+  (* low-cardinality-threshold field-values-entry-max-length))
 
-(def ^:private ^:const average-length-no-preview-threshold
+(def ^:private ^:const ^Integer average-length-no-preview-threshold
   "Fields whose values' average length is greater than this amount should be marked as `preview_display = false`."
   50)
 
@@ -52,6 +54,12 @@
            (not (isa? (:base_type field) :type/Collection))
            (not (= (:base_type field) :type/*)))))
 
+(defn- field-values-below-low-cardinality-threshold? [non-nil-values]
+  (and (<= (count non-nil-values) low-cardinality-threshold)
+      ;; very simple check to see if total length of field-values exceeds (total values * max per value)
+       (let [total-length (reduce + (map (comp count str) non-nil-values))]
+         (<= total-length field-values-total-max-length))))
+
 (defn test:cardinality-and-extract-field-values
   "Extract field-values for FIELD.  If number of values exceeds `low-cardinality-threshold` then we return an empty set of values."
   [field field-stats]
@@ -59,10 +67,7 @@
   ;;       for example, :type/Category fields with more than MAX values don't need to be rescanned all the time
   (let [non-nil-values  (filter identity (queries/field-distinct-values field (inc low-cardinality-threshold)))
         ;; only return the list if we didn't exceed our MAX values and if the the total character count of our values is reasable (#2332)
-        distinct-values (when-not (or (< low-cardinality-threshold (count non-nil-values))
-                                      ;; very simple check to see if total length of field-values exceeds (total values * max per value)
-                                      (< (* low-cardinality-threshold
-                                            field-values-entry-max-length) (reduce + (map (comp count str) non-nil-values))))
+        distinct-values (when (field-values-below-low-cardinality-threshold? non-nil-values)
                           non-nil-values)]
     (cond-> (assoc field-stats :values distinct-values)
       (and (nil? (:special_type field))
@@ -181,12 +186,13 @@
 (defn make-analyze-table
   "Make a generic implementation of `analyze-table`."
   {:style/indent 1}
-  [driver & {:keys [field-avg-length-fn field-percent-urls-fn]
+  [driver & {:keys [field-avg-length-fn field-percent-urls-fn calculate-row-count?]
              :or   {field-avg-length-fn   (partial driver/default-field-avg-length driver)
-                    field-percent-urls-fn (partial driver/default-field-percent-urls driver)}}]
+                    field-percent-urls-fn (partial driver/default-field-percent-urls driver)
+                    calculate-row-count?  true}}]
   (fn [driver table new-field-ids]
     (let [driver (assoc driver :field-avg-length field-avg-length-fn, :field-percent-urls field-percent-urls-fn)]
-      {:row_count (u/try-apply table-row-count table)
+      {:row_count (when calculate-row-count? (u/try-apply table-row-count table))
        :fields    (for [{:keys [id] :as field} (table/fields table)]
                     (let [new-field? (contains? new-field-ids id)]
                       (cond->> {:id id}
diff --git a/src/metabase/util.clj b/src/metabase/util.clj
index 993e311ee7345c395d357e0497ef820db8318ca2..d73dc84cf240d6b47312984f5ef8b545f9d7baaf 100644
--- a/src/metabase/util.clj
+++ b/src/metabase/util.clj
@@ -822,3 +822,12 @@
   "Increment N if it is non-`nil`, otherwise return `1` (e.g. as if incrementing `0`)."
   [n]
   (if n (inc n) 1))
+
+(defn occurances-of-substring
+  "Return the number of times SUBSTR occurs in string S."
+  ^Integer [^String s, ^String substr]
+  (when (and (seq s) (seq substr))
+    (loop [index 0, cnt 0]
+      (if-let [new-index (s/index-of s substr index)]
+        (recur (inc new-index) (inc cnt))
+        cnt))))
diff --git a/src/metabase/util/honeysql_extensions.clj b/src/metabase/util/honeysql_extensions.clj
index 66a5ccaef18b7da1b29dd32bd7fce661c6dd6aae..5b21d722754cf15f64f30df2d3a4e9a6e56e6331 100644
--- a/src/metabase/util/honeysql_extensions.clj
+++ b/src/metabase/util/honeysql_extensions.clj
@@ -92,10 +92,15 @@
   (hsql/call :cast x (hsql/raw (name c))))
 
 (defn format
-  "SQL `FORMAT` function."
+  "SQL `format` function."
   [format-str expr]
   (hsql/call :format expr (literal format-str)))
 
+(defn round
+  "SQL `round` function."
+  [x decimal-places]
+  (hsql/call :round x decimal-places))
+
 (defn ->date                     "CAST X to a `date`."                     [x] (cast :date x))
 (defn ->datetime                 "CAST X to a `datetime`."                 [x] (cast :datetime x))
 (defn ->timestamp                "CAST X to a `timestamp`."                [x] (cast :timestamp x))
diff --git a/src/metabase/util/urls.clj b/src/metabase/util/urls.clj
index db1c2948f50289fd71a4222729b61d59259f920c..3c27b5ee2033afecfc12bad23d45101565f0e1e0 100644
--- a/src/metabase/util/urls.clj
+++ b/src/metabase/util/urls.clj
@@ -18,16 +18,16 @@
 (defn dashboard-url
   "Return an appropriate URL for a `Dashboard` with ID.
 
-     (dashboard-url 10) -> \"http://localhost:3000/dash/10\""
+     (dashboard-url 10) -> \"http://localhost:3000/dashboard/10\""
   [^Integer id]
-  (format "%s/dash/%d" (public-settings/site-url) id))
+  (format "%s/dashboard/%d" (public-settings/site-url) id))
 
 (defn card-url
   "Return an appropriate URL for a `Card` with ID.
 
-     (card-url 10) -> \"http://localhost:3000/card/10\""
+     (card-url 10) -> \"http://localhost:3000/question/10\""
   [^Integer id]
-  (format "%s/card/%d" (public-settings/site-url) id))
+  (format "%s/question/%d" (public-settings/site-url) id))
 
 (defn segment-url
   "Return an appropriate URL for a `Segment` with ID.
diff --git a/test/metabase/api/card_test.clj b/test/metabase/api/card_test.clj
index 34a759795d2831efa8e133687e49cebdbeaf7780..1a3bb3e25ce76b6cc7980a4b3f2899d09d88a7a5 100644
--- a/test/metabase/api/card_test.clj
+++ b/test/metabase/api/card_test.clj
@@ -36,7 +36,8 @@
    :embedding_params  nil
    :made_public_by_id nil
    :public_uuid       nil
-   :query_type        "query"})
+   :query_type        "query"
+   :cache_ttl         nil})
 
 ;; ## GET /api/card
 ;; Filter cards by database
diff --git a/test/metabase/api/dataset_test.clj b/test/metabase/api/dataset_test.clj
index 20a86830f56f070d9df5a9b72ec7eb20930ce132..637acfad10827deecd13941e0ba37f3ff514f5b3 100644
--- a/test/metabase/api/dataset_test.clj
+++ b/test/metabase/api/dataset_test.clj
@@ -52,22 +52,23 @@
 ;; Just a basic sanity check to make sure Query Processor endpoint is still working correctly.
 (expect
   [;; API call response
-   {:data         {:rows    [[1000]]
-                   :columns ["count"]
-                   :cols    [{:base_type "type/Integer", :special_type "type/Number", :name "count", :display_name "count", :id nil, :table_id nil,
-                              :description nil, :target nil, :extra_info {}, :source "aggregation"}]
-                   :native_form true}
-    :row_count    1
-    :status       "completed"
-    :context      "ad-hoc"
-    :json_query   (-> (wrap-inner-query
-                        (query checkins
-                          (ql/aggregation (ql/count))))
-                      (assoc :type "query")
-                      (assoc-in [:query :aggregation] [{:aggregation-type "count", :custom-name nil}])
-                      (assoc :constraints default-query-constraints))
-    :started_at   true
-    :running_time true}
+   {:data                   {:rows    [[1000]]
+                             :columns ["count"]
+                             :cols    [{:base_type "type/Integer", :special_type "type/Number", :name "count", :display_name "count", :id nil, :table_id nil,
+                                        :description nil, :target nil, :extra_info {}, :source "aggregation"}]
+                             :native_form true}
+    :row_count              1
+    :status                 "completed"
+    :context                "ad-hoc"
+    :json_query             (-> (wrap-inner-query
+                                  (query checkins
+                                    (ql/aggregation (ql/count))))
+                                (assoc :type "query")
+                                (assoc-in [:query :aggregation] [{:aggregation-type "count", :custom-name nil}])
+                                (assoc :constraints default-query-constraints))
+    :started_at             true
+    :running_time           true
+    :average_execution_time nil}
    ;; QueryExecution record in the DB
    {:hash         true
     :row_count    1
diff --git a/test/metabase/api/metric_test.clj b/test/metabase/api/metric_test.clj
index 42b5676409694c06bcd216a0f9364094e7f9158e..925031a4d499c1e05ae67debece2cf3a656adf08 100644
--- a/test/metabase/api/metric_test.clj
+++ b/test/metabase/api/metric_test.clj
@@ -9,7 +9,7 @@
                              [metric :refer [Metric], :as metric]
                              [revision :refer [Revision]]
                              [table :refer [Table]])
-            [metabase.test.data :refer :all]
+            [metabase.test.data :refer :all, :as data]
             [metabase.test.data.users :refer :all]
             [metabase.test.util :as tu]))
 
@@ -372,6 +372,7 @@
 (tt/expect-with-temp [Metric [metric-1 {:name "Metric A"}]
                       Metric [metric-2 {:name "Metric B"}]
                       Metric [_        {:is_active false}]] ; inactive metrics shouldn't show up
-  (tu/mappify (hydrate [metric-1
-                        metric-2] :creator))
+  (tu/mappify (hydrate [(assoc metric-1 :database_id (data/id))
+                        (assoc metric-2 :database_id (data/id))]
+                       :creator))
   ((user->client :rasta) :get 200 "metric/"))
diff --git a/test/metabase/api/pulse_test.clj b/test/metabase/api/pulse_test.clj
index 01c62c0818b8878f93ca5c24423b4de4ed4246c5..08083bbb6651d03eed26fdd9a6d94da0ba7daec3 100644
--- a/test/metabase/api/pulse_test.clj
+++ b/test/metabase/api/pulse_test.clj
@@ -27,14 +27,15 @@
 
 (defn- pulse-details [pulse]
   (tu/match-$ pulse
-    {:id           $
-     :name         $
-     :created_at   $
-     :updated_at   $
-     :creator_id   $
-     :creator      (user-details (db/select-one 'User :id (:creator_id pulse)))
-     :cards        (map pulse-card-details (:cards pulse))
-     :channels     (map pulse-channel-details (:channels pulse))}))
+    {:id            $
+     :name          $
+     :created_at    $
+     :updated_at    $
+     :creator_id    $
+     :creator       (user-details (db/select-one 'User :id (:creator_id pulse)))
+     :cards         (map pulse-card-details (:cards pulse))
+     :channels      (map pulse-channel-details (:channels pulse))
+     :skip_if_empty $}))
 
 (defn- pulse-response [{:keys [created_at updated_at], :as pulse}]
   (-> pulse
@@ -87,27 +88,29 @@
 
 (tt/expect-with-temp [Card [card1]
                       Card [card2]]
-  {:name         "A Pulse"
-   :creator_id   (user->id :rasta)
-   :creator      (user-details (fetch-user :rasta))
-   :created_at   true
-   :updated_at   true
-   :cards        (mapv pulse-card-details [card1 card2])
-   :channels     [{:enabled        true
-                   :channel_type   "email"
-                   :schedule_type  "daily"
-                   :schedule_hour  12
-                   :schedule_day   nil
-                   :schedule_frame nil
-                   :recipients     []}]}
-  (-> (pulse-response ((user->client :rasta) :post 200 "pulse" {:name     "A Pulse"
-                                                                :cards    [{:id (:id card1)} {:id (:id card2)}]
-                                                                :channels [{:enabled       true
-                                                                            :channel_type  "email"
-                                                                            :schedule_type "daily"
-                                                                            :schedule_hour 12
-                                                                            :schedule_day  nil
-                                                                            :recipients    []}]}))
+  {:name          "A Pulse"
+   :creator_id    (user->id :rasta)
+   :creator       (user-details (fetch-user :rasta))
+   :created_at    true
+   :updated_at    true
+   :cards         (mapv pulse-card-details [card1 card2])
+   :channels      [{:enabled        true
+                    :channel_type   "email"
+                    :schedule_type  "daily"
+                    :schedule_hour  12
+                    :schedule_day   nil
+                    :schedule_frame nil
+                    :recipients     []}]
+   :skip_if_empty false}
+  (-> (pulse-response ((user->client :rasta) :post 200 "pulse" {:name          "A Pulse"
+                                                                :cards         [{:id (:id card1)} {:id (:id card2)}]
+                                                                :channels      [{:enabled       true
+                                                                                 :channel_type  "email"
+                                                                                 :schedule_type "daily"
+                                                                                 :schedule_hour 12
+                                                                                 :schedule_day  nil
+                                                                                 :recipients    []}]
+                                                                :skip_if_empty false}))
       (update :channels remove-extra-channels-fields)))
 
 
@@ -143,29 +146,31 @@
 
 (tt/expect-with-temp [Pulse [pulse]
                       Card  [card]]
-  {:name         "Updated Pulse"
-   :creator_id   (user->id :rasta)
-   :creator      (user-details (fetch-user :rasta))
-   :created_at   true
-   :updated_at   true
-   :cards        [(pulse-card-details card)]
-   :channels     [{:enabled       true
-                   :channel_type  "slack"
-                   :schedule_type "hourly"
-                   :schedule_hour nil
-                   :schedule_day  nil
-                   :schedule_frame nil
-                   :details       {:channels "#general"}
-                   :recipients    []}]}
-  (-> (pulse-response ((user->client :rasta) :put 200 (format "pulse/%d" (:id pulse)) {:name     "Updated Pulse"
-                                                                                       :cards    [{:id (:id card)}]
-                                                                                       :channels [{:enabled       true
-                                                                                                   :channel_type  "slack"
-                                                                                                   :schedule_type "hourly"
-                                                                                                   :schedule_hour 12
-                                                                                                   :schedule_day  "mon"
-                                                                                                   :recipients    []
-                                                                                                   :details       {:channels "#general"}}]}))
+  {:name          "Updated Pulse"
+   :creator_id    (user->id :rasta)
+   :creator       (user-details (fetch-user :rasta))
+   :created_at    true
+   :updated_at    true
+   :cards         [(pulse-card-details card)]
+   :channels      [{:enabled       true
+                    :channel_type  "slack"
+                    :schedule_type "hourly"
+                    :schedule_hour nil
+                    :schedule_day  nil
+                    :schedule_frame nil
+                    :details       {:channels "#general"}
+                    :recipients    []}]
+   :skip_if_empty false}
+  (-> (pulse-response ((user->client :rasta) :put 200 (format "pulse/%d" (:id pulse)) {:name          "Updated Pulse"
+                                                                                       :cards         [{:id (:id card)}]
+                                                                                       :channels      [{:enabled       true
+                                                                                                        :channel_type  "slack"
+                                                                                                        :schedule_type "hourly"
+                                                                                                        :schedule_hour 12
+                                                                                                        :schedule_day  "mon"
+                                                                                                        :recipients    []
+                                                                                                        :details       {:channels "#general"}}]
+                                                                                       :skip_if_empty false}))
       (update :channels remove-extra-channels-fields)))
 
 
diff --git a/test/metabase/cmd/load_from_h2_test.clj b/test/metabase/cmd/load_from_h2_test.clj
index c9a6c119457d11dc399285b187257b64bdddf860..3d3942f4773bce5f0f0b16c0e2fcd3145abd288e 100644
--- a/test/metabase/cmd/load_from_h2_test.clj
+++ b/test/metabase/cmd/load_from_h2_test.clj
@@ -13,7 +13,10 @@
 
 (def ^:private models-to-exclude
   "Models that should *not* be migrated in `load-from-h2`."
-  #{"LegacyQueryExecution"})
+  #{"LegacyQueryExecution"
+    "Query"
+    "QueryCache"
+    "QueryExecution"})
 
 (defn- all-model-names []
   (set (for [ns       (ns-find/find-namespaces (classpath/classpath))
diff --git a/test/metabase/driver/bigquery_test.clj b/test/metabase/driver/bigquery_test.clj
index d6767253f02cc9514439c1ae831eef331aeb3370..97fd5c353f76e42e5f0d4f75ae0ece8080f29f40 100644
--- a/test/metabase/driver/bigquery_test.clj
+++ b/test/metabase/driver/bigquery_test.clj
@@ -2,6 +2,7 @@
   (:require metabase.driver.bigquery
             [metabase.models.database :as database]
             [metabase.query-processor :as qp]
+            [metabase.query-processor-test :as qptest]
             [metabase.test.data :as data]
             (metabase.test.data [datasets :refer [expect-with-engine]]
                                 [interface :refer [def-database-definition]])))
@@ -29,3 +30,14 @@
                                          :type     :native
                                          :database (data/id)}))
                [:cols :columns]))
+
+;; make sure that the bigquery driver can handle named columns with characters that aren't allowed in BQ itself
+(expect-with-engine :bigquery
+  {:rows    [[113]]
+   :columns ["User_ID_Plus_Venue_ID"]}
+  (qptest/rows+column-names (qp/process-query {:database (data/id)
+                                               :type     "query"
+                                               :query    {:source_table (data/id :checkins)
+                                                          :aggregation  [["named" ["max" ["+" ["field-id" (data/id :checkins :user_id)]
+                                                                                              ["field-id" (data/id :checkins :venue_id)]]]
+                                                                                  "User ID Plus Venue ID"]]}})))
diff --git a/test/metabase/driver/generic_sql/util/unprepare_test.clj b/test/metabase/driver/generic_sql/util/unprepare_test.clj
index b20a4d91e9bbf2cad244e8258bd19c5de1c573de..ce98bb08df85eeff569c6943d0b1a9155d57d096 100644
--- a/test/metabase/driver/generic_sql/util/unprepare_test.clj
+++ b/test/metabase/driver/generic_sql/util/unprepare_test.clj
@@ -8,3 +8,12 @@
                         "Cam's Cool Toucan"
                         true
                         #inst "2017-01-01T00:00:00.000Z"]))
+
+(expect
+  "SELECT 'Cam''s Cool Toucan' FROM TRUE WHERE x ?? y AND z = from_iso8601_timestamp('2017-01-01T00:00:00.000Z')"
+  (unprepare/unprepare ["SELECT ? FROM ? WHERE x ?? y AND z = ?"
+                        "Cam's Cool Toucan"
+                        true
+                        #inst "2017-01-01T00:00:00.000Z"]
+                       :quote-escape "'"
+                       :iso-8601-fn  :from_iso8601_timestamp))
diff --git a/test/metabase/driver/generic_sql_test.clj b/test/metabase/driver/generic_sql_test.clj
index bbb68a74743a7d5775c342a05c8827bc168405f3..4c51952cf9e49066ce8c3f5276433e1511db5218 100644
--- a/test/metabase/driver/generic_sql_test.clj
+++ b/test/metabase/driver/generic_sql_test.clj
@@ -19,7 +19,7 @@
 (def ^:private generic-sql-engines
   (delay (set (for [engine datasets/all-valid-engines
                     :let   [driver (driver/engine->driver engine)]
-                    :when  (not= engine :bigquery)                                       ; bigquery doesn't use the generic sql implementations of things like `field-avg-length`
+                    :when  (not (contains? #{:bigquery :presto} engine))                 ; bigquery and presto don't use the generic sql implementations of things like `field-avg-length`
                     :when  (extends? ISQLDriver (class driver))]
                 (do (require (symbol (str "metabase.test.data." (name engine))) :reload) ; otherwise it gets all snippy if you try to do `lein test metabase.driver.generic-sql-test`
                     engine)))))
diff --git a/test/metabase/driver/mongo_test.clj b/test/metabase/driver/mongo_test.clj
index 3332a298277eff2370b30b79f5f3f47781186cdc..1387e674bc6939b26d293749344eb763b1c821f4 100644
--- a/test/metabase/driver/mongo_test.clj
+++ b/test/metabase/driver/mongo_test.clj
@@ -175,21 +175,33 @@
             (ql/filter (ql/= $bird_id "abcdefabcdefabcdefabcdef"))))))
 
 
-;;; ------------------------------------------------------------ Test that we can handle native queries with "ISODate(...)" forms (#3741) ------------------------------------------------------------
+;;; ------------------------------------------------------------ Test that we can handle native queries with "ISODate(...)" and "ObjectId(...) forms (#3741, #4448) ------------------------------------------------------------
 (tu/resolve-private-vars metabase.driver.mongo.query-processor
-  maybe-decode-iso-date-fncall decode-iso-date-fncalls encode-iso-date-fncalls)
+  maybe-decode-fncall decode-fncalls encode-fncalls)
 
 (expect
   "[{\"$match\":{\"date\":{\"$gte\":[\"___ISODate\", \"2012-01-01\"]}}}]"
-  (encode-iso-date-fncalls "[{\"$match\":{\"date\":{\"$gte\":ISODate(\"2012-01-01\")}}}]"))
+  (encode-fncalls "[{\"$match\":{\"date\":{\"$gte\":ISODate(\"2012-01-01\")}}}]"))
+
+(expect
+  "[{\"$match\":{\"entityId\":{\"$eq\":[\"___ObjectId\", \"583327789137b2700a1621fb\"]}}}]"
+  (encode-fncalls "[{\"$match\":{\"entityId\":{\"$eq\":ObjectId(\"583327789137b2700a1621fb\")}}}]"))
 
 (expect
   (DateTime. "2012-01-01")
-  (maybe-decode-iso-date-fncall ["___ISODate" "2012-01-01"]))
+  (maybe-decode-fncall ["___ISODate" "2012-01-01"]))
+
+(expect
+  (ObjectId. "583327789137b2700a1621fb")
+  (maybe-decode-fncall ["___ObjectId" "583327789137b2700a1621fb"]))
 
 (expect
   [{:$match {:date {:$gte (DateTime. "2012-01-01")}}}]
-  (decode-iso-date-fncalls [{:$match {:date {:$gte ["___ISODate" "2012-01-01"]}}}]))
+  (decode-fncalls [{:$match {:date {:$gte ["___ISODate" "2012-01-01"]}}}]))
+
+(expect
+  [{:$match {:entityId {:$eq (ObjectId. "583327789137b2700a1621fb")}}}]
+  (decode-fncalls [{:$match {:entityId {:$eq ["___ObjectId" "583327789137b2700a1621fb"]}}}]))
 
 (datasets/expect-with-engine :mongo
   5
@@ -197,3 +209,11 @@
                                              :collection "checkins"}
                                   :type     :native
                                   :database (data/id)}))))
+
+(datasets/expect-with-engine :mongo
+  0
+  ;; this query shouldn't match anything, so we're just checking that it completes successfully
+  (count (rows (qp/process-query {:native   {:query      "[{\"$match\": {\"_id\": {\"$eq\": ObjectId(\"583327789137b2700a1621fb\")}}}]"
+                                             :collection "venues"}
+                                  :type     :native
+                                  :database (data/id)}))))
diff --git a/test/metabase/driver/presto_test.clj b/test/metabase/driver/presto_test.clj
new file mode 100644
index 0000000000000000000000000000000000000000..86502b962ef26390c1e6f3ae9c777b69409dc94d
--- /dev/null
+++ b/test/metabase/driver/presto_test.clj
@@ -0,0 +1,143 @@
+(ns metabase.driver.presto-test
+  (:require [expectations :refer :all]
+            [toucan.db :as db]
+            [metabase.driver :as driver]
+            [metabase.driver.generic-sql :as sql]
+            [metabase.models.table :as table]
+            [metabase.test.data :as data]
+            [metabase.test.data.datasets :as datasets]
+            [metabase.test.util :refer [resolve-private-vars]])
+  (:import (metabase.driver.presto PrestoDriver)))
+
+(resolve-private-vars metabase.driver.presto details->uri details->request parse-presto-results quote-name quote+combine-names apply-page)
+
+;;; HELPERS
+
+(expect
+  "http://localhost:8080/"
+  (details->uri {:host "localhost", :port 8080, :ssl false} "/"))
+
+(expect
+  "https://localhost:8443/"
+  (details->uri {:host "localhost", :port 8443, :ssl true} "/"))
+
+(expect
+  "http://localhost:8080/v1/statement"
+  (details->uri {:host "localhost", :port 8080, :ssl false} "/v1/statement"))
+
+(expect
+  {:headers {"X-Presto-Source" "metabase"
+             "X-Presto-User"   "user"}}
+  (details->request {:user "user"}))
+
+(expect
+  {:headers    {"X-Presto-Source" "metabase"
+                "X-Presto-User"   "user"}
+   :basic-auth ["user" "test"]}
+  (details->request {:user "user", :password "test"}))
+
+(expect
+  {:headers {"X-Presto-Source"    "metabase"
+             "X-Presto-User"      "user"
+             "X-Presto-Catalog"   "test_data"
+             "X-Presto-Time-Zone" "America/Toronto"}}
+  (details->request {:user "user", :catalog "test_data", :report-timezone "America/Toronto"}))
+
+(expect
+  [["2017-04-03"
+    #inst "2017-04-03T14:19:17.417000000-00:00"
+    #inst "2017-04-03T10:19:17.417000000-00:00"
+    3.1416M
+    "test"]]
+  (parse-presto-results [{:type "date"} {:type "timestamp with time zone"} {:type "timestamp"} {:type "decimal(10,4)"} {:type "varchar(255)"}]
+                        [["2017-04-03", "2017-04-03 10:19:17.417 America/Toronto", "2017-04-03 10:19:17.417", "3.1416", "test"]]))
+
+(expect
+  "\"weird.table\"\" name\""
+  (quote-name "weird.table\" name"))
+
+(expect
+  "\"weird . \"\"schema\".\"weird.table\"\" name\""
+  (quote+combine-names "weird . \"schema" "weird.table\" name"))
+
+;; DESCRIBE-DATABASE
+(datasets/expect-with-engine :presto
+  {:tables #{{:name "categories" :schema "default"}
+             {:name "venues"     :schema "default"}
+             {:name "checkins"   :schema "default"}
+             {:name "users"      :schema "default"}}}
+  (driver/describe-database (PrestoDriver.) (data/db)))
+
+;; DESCRIBE-TABLE
+(datasets/expect-with-engine :presto
+  {:name   "venues"
+   :schema "default"
+   :fields #{{:name      "name",
+              :base-type :type/Text}
+             {:name      "latitude"
+              :base-type :type/Float}
+             {:name      "longitude"
+              :base-type :type/Float}
+             {:name      "price"
+              :base-type :type/Integer}
+             {:name      "category_id"
+              :base-type :type/Integer}
+             {:name      "id"
+              :base-type :type/Integer}}}
+  (driver/describe-table (PrestoDriver.) (data/db) (db/select-one 'Table :id (data/id :venues))))
+
+;;; ANALYZE-TABLE
+(datasets/expect-with-engine :presto
+  {:row_count 100
+   :fields    [{:id (data/id :venues :category_id), :values [2 3 4 5 6 7 10 11 12 13 14 15 18 19 20 29 40 43 44 46 48 49 50 58 64 67 71 74]}
+               {:id (data/id :venues :id)}
+               {:id (data/id :venues :latitude)}
+               {:id (data/id :venues :longitude)}
+               {:id (data/id :venues :name), :values (db/select-one-field :values 'FieldValues, :field_id (data/id :venues :name))}
+               {:id (data/id :venues :price), :values [1 2 3 4]}]}
+  (let [venues-table (db/select-one 'Table :id (data/id :venues))]
+    (driver/analyze-table (PrestoDriver.) venues-table (set (mapv :id (table/fields venues-table))))))
+
+;;; FIELD-VALUES-LAZY-SEQ
+(datasets/expect-with-engine :presto
+  ["Red Medicine"
+   "Stout Burgers & Beers"
+   "The Apple Pan"
+   "Wurstküche"
+   "Brite Spot Family Restaurant"]
+  (take 5 (driver/field-values-lazy-seq (PrestoDriver.) (db/select-one 'Field :id (data/id :venues :name)))))
+
+;;; TABLE-ROWS-SEQ
+(datasets/expect-with-engine :presto
+  [{:name "Red Medicine",                 :price 3, :category_id  4, :id 1}
+   {:name "Stout Burgers & Beers",        :price 2, :category_id 11, :id 2}
+   {:name "The Apple Pan",                :price 2, :category_id 11, :id 3}
+   {:name "Wurstküche",                   :price 2, :category_id 29, :id 4}
+   {:name "Brite Spot Family Restaurant", :price 2, :category_id 20, :id 5}]
+  (for [row (take 5 (sort-by :id (driver/table-rows-seq (PrestoDriver.)
+                                                        (db/select-one 'Database :id (data/id))
+                                                        (db/select-one 'RawTable :id (db/select-one-field :raw_table_id 'Table, :id (data/id :venues))))))]
+    (-> (dissoc row :latitude :longitude)
+        (update :price int)
+        (update :category_id int)
+        (update :id int))))
+
+;;; FIELD-PERCENT-URLS
+(datasets/expect-with-engine :presto
+  0.5
+  (data/dataset half-valid-urls
+    (sql/field-percent-urls (PrestoDriver.) (db/select-one 'Field :id (data/id :urls :url)))))
+
+;;; APPLY-PAGE
+(expect
+  {:select ["name" "id"]
+   :from   [{:select   [[:default.categories.name "name"] [:default.categories.id "id"] [{:s "row_number() OVER (ORDER BY \"default\".\"categories\".\"id\" ASC)"} :__rownum__]]
+             :from     [:default.categories]
+             :order-by [[:default.categories.id :asc]]}]
+   :where  [:> :__rownum__ 5]
+   :limit  5}
+  (apply-page {:select   [[:default.categories.name "name"] [:default.categories.id "id"]]
+               :from     [:default.categories]
+               :order-by [[:default.categories.id :asc]]}
+              {:page {:page  2
+                      :items 5}}))
diff --git a/test/metabase/events/revision_test.clj b/test/metabase/events/revision_test.clj
index 72c08795905261c829981144aba90e986bdc4abc..ef56ae4507f773109b1879a3d40dd5d45ef6f5bc 100644
--- a/test/metabase/events/revision_test.clj
+++ b/test/metabase/events/revision_test.clj
@@ -41,6 +41,7 @@
    :made_public_by_id      nil
    :name                   (:name card)
    :public_uuid            nil
+   :cache_ttl              nil
    :query_type             "query"
    :table_id               (id :categories)
    :visualization_settings {}})
diff --git a/test/metabase/models/pulse_test.clj b/test/metabase/models/pulse_test.clj
index 996f2a64ab5d0d900fd1b078b39ea0464de4b7a2..1273f76fdf9ee15e90b83c403a3bbb60bb45b371 100644
--- a/test/metabase/models/pulse_test.clj
+++ b/test/metabase/models/pulse_test.clj
@@ -20,8 +20,8 @@
 
 ;; create a channel then select its details
 (defn- create-pulse-then-select!
-  [name creator cards channels]
-  (let [{:keys [cards channels] :as pulse} (create-pulse! name creator cards channels)]
+  [name creator cards channels skip-if-empty?]
+  (let [{:keys [cards channels] :as pulse} (create-pulse! name creator cards channels skip-if-empty?)]
     (-> pulse
         (dissoc :id :creator :created_at :updated_at)
         (assoc :cards (mapv #(dissoc % :id) cards))
@@ -43,21 +43,22 @@
 ;; retrieve-pulse
 ;; this should cover all the basic Pulse attributes
 (expect
-  {:creator_id   (user->id :rasta)
-   :creator      (user-details :rasta)
-   :name         "Lodi Dodi"
-   :cards        [{:name        "Test Card"
-                   :description nil
-                   :display     :table}]
-   :channels     [{:enabled        true
-                   :schedule_type  :daily
-                   :schedule_hour  15
-                   :schedule_frame nil
-                   :channel_type   :email
-                   :details        {:other "stuff"}
-                   :schedule_day   nil
-                   :recipients     [{:email "foo@bar.com"}
-                                    (dissoc (user-details :rasta) :is_superuser :is_qbnewb)]}]}
+  {:creator_id    (user->id :rasta)
+   :creator       (user-details :rasta)
+   :name          "Lodi Dodi"
+   :cards         [{:name        "Test Card"
+                    :description nil
+                    :display     :table}]
+   :channels      [{:enabled        true
+                    :schedule_type  :daily
+                    :schedule_hour  15
+                    :schedule_frame nil
+                    :channel_type   :email
+                    :details        {:other "stuff"}
+                    :schedule_day   nil
+                    :recipients     [{:email "foo@bar.com"}
+                                     (dissoc (user-details :rasta) :is_superuser :is_qbnewb)]}]
+   :skip_if_empty false}
   (tt/with-temp* [Pulse        [{pulse-id :id}               {:name "Lodi Dodi"}]
                   PulseChannel [{channel-id :id :as channel} {:pulse_id pulse-id
                                                               :details  {:other  "stuff"
@@ -119,23 +120,28 @@
 ;; create-pulse!
 ;; simple example with a single card
 (expect
-  {:creator_id (user->id :rasta)
-   :name       "Booyah!"
-   :channels   [{:enabled        true
-                 :schedule_type  :daily
-                 :schedule_hour  18
-                 :schedule_frame nil
-                 :channel_type   :email
-                 :recipients     [{:email "foo@bar.com"}]
-                 :schedule_day   nil}]
-   :cards      [{:name        "Test Card"
-                 :description nil
-                 :display     :table}]}
+  {:creator_id    (user->id :rasta)
+   :name          "Booyah!"
+   :channels      [{:enabled        true
+                    :schedule_type  :daily
+                    :schedule_hour  18
+                    :schedule_frame nil
+                    :channel_type   :email
+                    :recipients     [{:email "foo@bar.com"}]
+                    :schedule_day   nil}]
+   :cards         [{:name        "Test Card"
+                    :description nil
+                    :display     :table}]
+   :skip_if_empty false}
   (tt/with-temp Card [{:keys [id]} {:name "Test Card"}]
-    (create-pulse-then-select! "Booyah!" (user->id :rasta) [id] [{:channel_type  :email
-                                                                  :schedule_type :daily
-                                                                  :schedule_hour 18
-                                                                  :recipients    [{:email "foo@bar.com"}]}])))
+    (create-pulse-then-select! "Booyah!"
+                               (user->id :rasta)
+                               [id]
+                               [{:channel_type  :email
+                                 :schedule_type :daily
+                                 :schedule_hour 18
+                                 :recipients    [{:email "foo@bar.com"}]}]
+                               false)))
 
 ;; update-pulse!
 ;; basic update.  we are testing several things here
@@ -146,31 +152,33 @@
 ;;  5. ability to create new channels
 ;;  6. ability to update cards and ensure proper ordering
 (expect
-  {:creator_id   (user->id :rasta)
-   :name         "We like to party"
-   :cards        [{:name        "Bar Card"
-                   :description nil
-                   :display     :bar}
-                  {:name        "Test Card"
-                   :description nil
-                   :display     :table}]
-   :channels     [{:enabled        true
-                   :schedule_type  :daily
-                   :schedule_hour  18
-                   :schedule_frame nil
-                   :channel_type   :email
-                   :schedule_day   nil
-                   :recipients     [{:email "foo@bar.com"}
-                                    (dissoc (user-details :crowberto) :is_superuser :is_qbnewb)]}]}
+  {:creator_id    (user->id :rasta)
+   :name          "We like to party"
+   :cards         [{:name        "Bar Card"
+                    :description nil
+                    :display     :bar}
+                   {:name        "Test Card"
+                    :description nil
+                    :display     :table}]
+   :channels      [{:enabled        true
+                    :schedule_type  :daily
+                    :schedule_hour  18
+                    :schedule_frame nil
+                    :channel_type   :email
+                    :schedule_day   nil
+                    :recipients     [{:email "foo@bar.com"}
+                                     (dissoc (user-details :crowberto) :is_superuser :is_qbnewb)]}]
+   :skip_if_empty false}
   (tt/with-temp* [Pulse [{pulse-id :id}]
                   Card  [{card-id-1 :id} {:name "Test Card"}]
                   Card  [{card-id-2 :id} {:name "Bar Card", :display :bar}]]
-    (update-pulse-then-select! {:id         pulse-id
-                                :name       "We like to party"
-                                :creator_id (user->id :crowberto)
-                                :cards      [card-id-2 card-id-1]
-                                :channels   [{:channel_type  :email
-                                              :schedule_type :daily
-                                              :schedule_hour 18
-                                              :recipients    [{:email "foo@bar.com"}
-                                                              {:id (user->id :crowberto)}]}]})))
+    (update-pulse-then-select! {:id             pulse-id
+                                :name           "We like to party"
+                                :creator_id     (user->id :crowberto)
+                                :cards          [card-id-2 card-id-1]
+                                :channels       [{:channel_type  :email
+                                                  :schedule_type :daily
+                                                  :schedule_hour 18
+                                                  :recipients    [{:email "foo@bar.com"}
+                                                                  {:id (user->id :crowberto)}]}]
+                                :skip-if-empty? false})))
diff --git a/test/metabase/models/setting_test.clj b/test/metabase/models/setting_test.clj
index c4d6fb2ba8939c7bef523b19493a9ee24bb896fa..89210b0fb8f358348952d00cd84b912fa3a1b28a 100644
--- a/test/metabase/models/setting_test.clj
+++ b/test/metabase/models/setting_test.clj
@@ -238,3 +238,23 @@
   {:a 100, :b 200}
   (do (test-json-setting {:a 100, :b 200})
       (test-json-setting)))
+
+
+;;; make sure that if for some reason the cache gets out of sync it will reset so we can still set new settings values (#4178)
+
+(setting/defsetting ^:private toucan-name
+  "Name for the Metabase Toucan mascot.")
+
+(expect
+  "Banana Beak"
+  (do
+    ;; clear out any existing values of `toucan-name`
+    (db/simple-delete! setting/Setting {:key "toucan-name"})
+    ;; restore the cache
+    ((resolve 'metabase.models.setting/restore-cache-if-needed!))
+    ;; now set a value for the `toucan-name` setting the wrong way
+    (db/insert! setting/Setting {:key "toucan-name", :value "Rasta"})
+    ;; ok, now try to set the Setting the correct way
+    (toucan-name "Banana Beak")
+    ;; ok, make sure the setting was set
+    (toucan-name)))
diff --git a/test/metabase/permissions_collection_test.clj b/test/metabase/permissions_collection_test.clj
index a4d86a18d98bf5b22125afd3990a91eb20302c4a..575f572aff5b0f7cc72f275214c2455874c91293 100644
--- a/test/metabase/permissions_collection_test.clj
+++ b/test/metabase/permissions_collection_test.clj
@@ -19,9 +19,9 @@
 (defn- api-call-was-successful? {:style/indent 0} [response]
   (when (and (string? response)
              (not= response "You don't have permissions to do that."))
-    (println "users in db:" (db/select-field :email 'User)) ; NOCOMMIT
     (println "RESPONSE:" response)) ; DEBUG
-  (not= response "You don't have permissions to do that."))
+  (and (not= response "You don't have permissions to do that.")
+       (not= response "Unauthenticated")))
 
 (defn- can-run-query? [username]
   (api-call-was-successful? ((test-users/user->client username) :post (format "card/%d/query" (u/get-id *card:db2-count-of-venues*)))))
@@ -52,6 +52,7 @@
 (perms-test/expect-with-test-data
   true
   (tt/with-temp Collection [collection]
+    (println "[In the occasionally failing test]") ; DEBUG
     (set-card-collection! collection)
     (permissions/grant-collection-read-permissions! (group/all-users) collection)
     (can-run-query? :rasta)))
diff --git a/test/metabase/publc_settings_test.clj b/test/metabase/publc_settings_test.clj
index dd97384d940c35d566b0d19c13f3efa1ff9b80c8..22249289c1791d4f04416d1b30683b8aa2314cc3 100644
--- a/test/metabase/publc_settings_test.clj
+++ b/test/metabase/publc_settings_test.clj
@@ -3,9 +3,35 @@
             [metabase.public-settings :as public-settings]
             [metabase.test.util :as tu]))
 
-;; double-check that setting the `site-url` setting will automatically strip off trailing slashes
+ ;; double-check that setting the `site-url` setting will automatically strip off trailing slashes
 (expect
   "http://localhost:3000"
   (tu/with-temporary-setting-values [site-url nil]
     (public-settings/site-url "http://localhost:3000/")
     (public-settings/site-url)))
+
+ ;; double-check that setting the `site-url` setting will prepend `http://` if no protocol was specified
+(expect
+  "http://localhost:3000"
+  (tu/with-temporary-setting-values [site-url nil]
+    (public-settings/site-url "localhost:3000")
+    (public-settings/site-url)))
+
+(expect
+  "http://localhost:3000"
+  (tu/with-temporary-setting-values [site-url nil]
+    (public-settings/site-url "localhost:3000")
+    (public-settings/site-url)))
+
+(expect
+  "http://localhost:3000"
+  (tu/with-temporary-setting-values [site-url nil]
+    (public-settings/site-url "http://localhost:3000")
+    (public-settings/site-url)))
+
+;; if https:// was specified it should keep it
+(expect
+  "https://localhost:3000"
+  (tu/with-temporary-setting-values [site-url nil]
+    (public-settings/site-url "https://localhost:3000")
+    (public-settings/site-url)))
diff --git a/test/metabase/query_processor/middleware/cache_test.clj b/test/metabase/query_processor/middleware/cache_test.clj
new file mode 100644
index 0000000000000000000000000000000000000000..9a2f3afe91e5fcb54beedccd69ec417579ac5ef2
--- /dev/null
+++ b/test/metabase/query_processor/middleware/cache_test.clj
@@ -0,0 +1,187 @@
+(ns metabase.query-processor.middleware.cache-test
+  "Tests for the Query Processor cache."
+  (:require [expectations :refer :all]
+            [toucan.db :as db]
+            [metabase.models.query-cache :refer [QueryCache]]
+            [metabase.query-processor.middleware.cache :as cache]
+            [metabase.test.util :as tu]))
+
+(tu/resolve-private-vars metabase.query-processor.middleware.cache
+  is-cacheable?
+  results-are-below-max-byte-threshold?)
+
+(def ^:private mock-results
+  {:row_count 8
+   :status    :completed
+   :data      {:rows [[:toucan      71]
+                      [:bald-eagle  92]
+                      [:hummingbird 11]
+                      [:owl         10]
+                      [:chicken     69]
+                      [:robin       96]
+                      [:osprey      72]
+                      [:flamingo    70]]}})
+
+(def ^:private ^:dynamic ^Integer *query-execution-delay-ms* 0)
+
+(defn- mock-qp [& _]
+  (Thread/sleep *query-execution-delay-ms*)
+  mock-results)
+
+(def ^:private maybe-return-cached-results (cache/maybe-return-cached-results mock-qp))
+
+(defn- clear-cache! [] (db/simple-delete! QueryCache))
+
+(defn- cached? [results]
+  (if (:cached results)
+    :cached
+    :not-cached))
+
+(defn- run-query [& {:as query-kvs}]
+  (cached? (maybe-return-cached-results (merge {:cache_ttl 60, :query :abc} query-kvs))))
+
+
+;;; ------------------------------------------------------------ tests for is-cacheable? ------------------------------------------------------------
+
+;; something is-cacheable? if it includes a cach_ttl and the caching setting is enabled
+(expect
+  (tu/with-temporary-setting-values [enable-query-caching true]
+    (is-cacheable? {:cache_ttl 100})))
+
+(expect
+  false
+  (tu/with-temporary-setting-values [enable-query-caching false]
+    (is-cacheable? {:cache_ttl 100})))
+
+(expect
+  false
+  (tu/with-temporary-setting-values [enable-query-caching true]
+    (is-cacheable? {:cache_ttl nil})))
+
+
+;;; ------------------------------------------------------------ results-are-below-max-byte-threshold? ------------------------------------------------------------
+
+(expect
+  (tu/with-temporary-setting-values [query-caching-max-kb 128]
+    (results-are-below-max-byte-threshold? {:data {:rows [[1 "ABCDEF"]
+                                                          [3 "GHIJKL"]]}})))
+
+(expect
+  false
+  (tu/with-temporary-setting-values [query-caching-max-kb 1]
+    (results-are-below-max-byte-threshold? {:data {:rows (repeat 500 [1 "ABCDEF"])}})))
+
+;; check that `results-are-below-max-byte-threshold?` is lazy and fails fast if the query is over the threshold rather than serializing the entire thing
+(expect
+  false
+  (let [lazy-seq-realized? (atom false)]
+    (tu/with-temporary-setting-values [query-caching-max-kb 1]
+      (results-are-below-max-byte-threshold? {:data {:rows (lazy-cat (repeat 500 [1 "ABCDEF"])
+                                                                     (do (reset! lazy-seq-realized? true)
+                                                                         [2 "GHIJKL"]))}})
+      @lazy-seq-realized?)))
+
+
+;;; ------------------------------------------------------------ End-to-end middleware tests ------------------------------------------------------------
+
+;; if there's nothing in the cache, cached results should *not* be returned
+(expect
+  :not-cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-min-ttl 0]
+    (clear-cache!)
+    (run-query)))
+
+;; if we run the query twice, the second run should return cached results
+(expect
+  :cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-min-ttl 0]
+    (clear-cache!)
+    (run-query)
+    (run-query)))
+
+;; ...but if the cache entry is past it's TTL, the cached results shouldn't be returned
+(expect
+  :not-cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-min-ttl 0]
+    (clear-cache!)
+    (run-query :cache_ttl 1)
+    (Thread/sleep 2000)
+    (run-query :cache_ttl 1)))
+
+;; if caching is disabled then cache shouldn't be used even if there's something valid in there
+(expect
+  :not-cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-min-ttl 0]
+    (clear-cache!)
+    (run-query)
+    (tu/with-temporary-setting-values [enable-query-caching  false
+                                       query-caching-min-ttl 0]
+      (run-query))))
+
+
+;; check that `query-caching-max-kb` is respected and queries aren't cached if they're past the threshold
+(expect
+  :not-cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-max-kb  0
+                                     query-caching-min-ttl 0]
+    (clear-cache!)
+    (run-query)
+    (run-query)))
+
+;; check that `query-caching-max-ttl` is respected. Whenever a new query is cached the cache should evict any entries older that `query-caching-max-ttl`.
+;; Set max-ttl to one second, run query `:abc`, then wait two seconds, and run `:def`. This should trigger the cache flush for entries past `:max-ttl`;
+;; and the cached entry for `:abc` should be deleted. Running `:abc` a subsequent time should not return cached results
+(expect
+  :not-cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-max-ttl 1
+                                     query-caching-min-ttl 0]
+    (clear-cache!)
+    (run-query)
+    (Thread/sleep 2000)
+    (run-query, :query :def)
+    (run-query)))
+
+;; check that *ignore-cached-results* is respected when returning results...
+(expect
+  :not-cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-min-ttl 0]
+    (clear-cache!)
+    (run-query)
+    (binding [cache/*ignore-cached-results* true]
+      (run-query))))
+
+;; ...but if it's set those results should still be cached for next time.
+(expect
+  :cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-min-ttl 0]
+    (clear-cache!)
+    (binding [cache/*ignore-cached-results* true]
+      (run-query))
+    (run-query)))
+
+;; if the cache takes less than the min TTL to execute, it shouldn't be cached
+(expect
+  :not-cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-min-ttl 60]
+    (clear-cache!)
+    (run-query)
+    (run-query)))
+
+;; ...but if it takes *longer* than the min TTL, it should be cached
+(expect
+  :cached
+  (tu/with-temporary-setting-values [enable-query-caching  true
+                                     query-caching-min-ttl 1]
+    (binding [*query-execution-delay-ms* 1200]
+      (clear-cache!)
+      (run-query)
+      (run-query))))
diff --git a/test/metabase/query_processor/parameters_test.clj b/test/metabase/query_processor/parameters_test.clj
index f5262498246a10d42c023f4ea490c976b60d5de0..c4d1ca106c86e7dac4d462e798bcc24ad6f06b49 100644
--- a/test/metabase/query_processor/parameters_test.clj
+++ b/test/metabase/query_processor/parameters_test.clj
@@ -17,30 +17,45 @@
                                 [users :refer :all])
             [metabase.test.util :as tu]))
 
-(tu/resolve-private-vars metabase.query-processor.parameters
-  absolute-date->range relative-date->range)
-
-(expect {:end "2016-03-31", :start "2016-01-01"} (absolute-date->range "Q1-2016"))
-(expect {:end "2016-02-29", :start "2016-02-01"} (absolute-date->range "2016-02"))
-(expect {:end "2016-04-18", :start "2016-04-18"} (absolute-date->range "2016-04-18"))
-(expect {:end "2016-04-23", :start "2016-04-18"} (absolute-date->range "2016-04-18~2016-04-23"))
-
 ;; we hard code "now" to a specific point in time so that we can control the test output
-(defn- test-relative [value]
+(defn- test-date->range [value]
   (with-redefs-fn {#'clj-time.core/now (fn [] (t/date-time 2016 06 07 12 0 0))}
-    #(relative-date->range value nil)))
-
-(expect {:end "2016-06-06", :start "2016-05-31"} (test-relative "past7days"))
-(expect {:end "2016-06-06", :start "2016-05-08"} (test-relative "past30days"))
-(expect {:end "2016-06-11", :start "2016-06-05"} (test-relative "thisweek"))
-(expect {:end "2016-06-30", :start "2016-06-01"} (test-relative "thismonth"))
-(expect {:end "2016-12-31", :start "2016-01-01"} (test-relative "thisyear"))
-(expect {:end "2016-06-04", :start "2016-05-29"} (test-relative "lastweek"))
-(expect {:end "2016-05-31", :start "2016-05-01"} (test-relative "lastmonth"))
-(expect {:end "2015-12-31", :start "2015-01-01"} (test-relative "lastyear"))
-(expect {:end "2016-06-06", :start "2016-06-06"} (test-relative "yesterday"))
-(expect {:end "2016-06-07", :start "2016-06-07"} (test-relative "today"))
-
+    #(date-string->range value nil)))
+
+(expect {:end "2016-03-31", :start "2016-01-01"} (test-date->range "Q1-2016"))
+(expect {:end "2016-02-29", :start "2016-02-01"} (test-date->range "2016-02"))
+(expect {:end "2016-04-18", :start "2016-04-18"} (test-date->range "2016-04-18"))
+(expect {:end "2016-04-23", :start "2016-04-18"} (test-date->range "2016-04-18~2016-04-23"))
+(expect {:end "2016-04-23", :start "2016-04-18"} (test-date->range "2016-04-18~2016-04-23"))
+(expect {:start "2016-04-18"}                    (test-date->range "2016-04-18~"))
+(expect {:end "2016-04-18"}                      (test-date->range "~2016-04-18"))
+
+(expect {:end "2016-06-06", :start "2016-06-04"} (test-date->range "past3days"))
+(expect {:end "2016-06-06", :start "2016-05-31"} (test-date->range "past7days"))
+(expect {:end "2016-06-06", :start "2016-05-08"} (test-date->range "past30days"))
+(expect {:end "2016-05-31", :start "2016-04-01"} (test-date->range "past2months"))
+(expect {:end "2016-05-31", :start "2015-05-01"} (test-date->range "past13months"))
+(expect {:end "2015-12-31", :start "2015-01-01"} (test-date->range "past1years"))
+(expect {:end "2015-12-31", :start "2000-01-01"} (test-date->range "past16years"))
+
+(expect {:end "2016-06-10", :start "2016-06-08"} (test-date->range "next3days"))
+(expect {:end "2016-06-14", :start "2016-06-08"} (test-date->range "next7days"))
+(expect {:end "2016-07-07", :start "2016-06-08"} (test-date->range "next30days"))
+(expect {:end "2016-08-31", :start "2016-07-01"} (test-date->range "next2months"))
+(expect {:end "2017-07-31", :start "2016-07-01"} (test-date->range "next13months"))
+(expect {:end "2017-12-31", :start "2017-01-01"} (test-date->range "next1years"))
+(expect {:end "2032-12-31", :start "2017-01-01"} (test-date->range "next16years"))
+
+(expect {:end "2016-06-07", :start "2016-06-07"} (test-date->range "thisday"))
+(expect {:end "2016-06-11", :start "2016-06-05"} (test-date->range "thisweek"))
+(expect {:end "2016-06-30", :start "2016-06-01"} (test-date->range "thismonth"))
+(expect {:end "2016-12-31", :start "2016-01-01"} (test-date->range "thisyear"))
+
+(expect {:end "2016-06-04", :start "2016-05-29"} (test-date->range "lastweek"))
+(expect {:end "2016-05-31", :start "2016-05-01"} (test-date->range "lastmonth"))
+(expect {:end "2015-12-31", :start "2015-01-01"} (test-date->range "lastyear"))
+(expect {:end "2016-06-06", :start "2016-06-06"} (test-date->range "yesterday"))
+(expect {:end "2016-06-07", :start "2016-06-07"} (test-date->range "today"))
 
 ;;; +-------------------------------------------------------------------------------------------------------+
 ;;; |                                             MBQL QUERIES                                              |
diff --git a/test/metabase/query_processor/sql_parameters_test.clj b/test/metabase/query_processor/sql_parameters_test.clj
index 679f1bc3b328d35cdce8c4c423a62d0e64c89249..dda33ecf8b2442c1e0c354b4914fd7f1595774aa 100644
--- a/test/metabase/query_processor/sql_parameters_test.clj
+++ b/test/metabase/query_processor/sql_parameters_test.clj
@@ -315,7 +315,6 @@
             #inst "2016-08-01T00:00:00.000000000-00:00"]}
   (expand-with-dimension-param {:type "date/range", :value "2016-07-01~2016-08-01"}))
 
-
 ;; dimension (date/month-year)
 (expect
   {:query  "SELECT * FROM checkins WHERE CAST(\"PUBLIC\".\"CHECKINS\".\"DATE\" AS date) BETWEEN ? AND ?;"
@@ -330,6 +329,18 @@
             #inst "2016-03-31T00:00:00.000000000-00:00"]}
   (expand-with-dimension-param {:type "date/quarter-year", :value "Q1-2016"}))
 
+;; dimension (date/all-options, before)
+(expect
+  {:query  "SELECT * FROM checkins WHERE CAST(\"PUBLIC\".\"CHECKINS\".\"DATE\" AS date) < ?;"
+   :params [#inst "2016-07-01T00:00:00.000000000-00:00"]}
+  (expand-with-dimension-param {:type "date/all-options", :value "~2016-07-01"}))
+
+;; dimension (date/all-options, after)
+(expect
+  {:query  "SELECT * FROM checkins WHERE CAST(\"PUBLIC\".\"CHECKINS\".\"DATE\" AS date) > ?;"
+   :params [#inst "2016-07-01T00:00:00.000000000-00:00"]}
+  (expand-with-dimension-param {:type "date/all-options", :value "2016-07-01~"}))
+
 ;; relative date -- "yesterday"
 (expect
   {:query  "SELECT * FROM checkins WHERE CAST(\"PUBLIC\".\"CHECKINS\".\"DATE\" AS date) = ?;"
@@ -417,9 +428,10 @@
   (generic-sql/quote-name datasets/*driver* identifier))
 
 (defn- checkins-identifier []
-  ;; HACK ! I don't have all day to write protocol methods to make this work the "right" way so for BigQuery we will just hackily return the correct identifier here
-  (if (= datasets/*engine* :bigquery)
-    "[test_data.checkins]"
+  ;; HACK ! I don't have all day to write protocol methods to make this work the "right" way so for BigQuery and Presto we will just hackily return the correct identifier here
+  (case datasets/*engine*
+    :bigquery "[test_data.checkins]"
+    :presto   "\"default\".\"checkins\""
     (let [{table-name :name, schema :schema} (db/select-one ['Table :name :schema], :id (data/id :checkins))]
       (str (when (seq schema)
              (str (quote-name schema) \.))
@@ -516,3 +528,22 @@
                            :native     {:query         "SELECT count(*) FROM PRODUCTS WHERE TITLE LIKE {{x}}",
                                         :template_tags {:x {:name "x", :display_name "X", :type "text", :required true, :default "%Toucan%"}}},
                            :parameters [{:type "category", :target ["variable" ["template-tag" "x"]]}]})))
+
+;; make sure that you can use the same parameter multiple times (#4659)
+(expect
+  {:query         "SELECT count(*) FROM products WHERE title LIKE ? AND subtitle LIKE ?"
+   :template_tags {:x {:name "x", :display_name "X", :type "text", :required true, :default "%Toucan%"}}
+   :params        ["%Toucan%" "%Toucan%"]}
+  (:native (expand-params {:driver     (driver/engine->driver :h2)
+                           :native     {:query         "SELECT count(*) FROM products WHERE title LIKE {{x}} AND subtitle LIKE {{x}}",
+                                        :template_tags {:x {:name "x", :display_name "X", :type "text", :required true, :default "%Toucan%"}}},
+                           :parameters [{:type "category", :target ["variable" ["template-tag" "x"]]}]})))
+
+(expect
+  {:query         "SELECT * FROM ORDERS WHERE true  AND ID = ? OR USER_ID = ?"
+   :template_tags {:id {:name "id", :display_name "ID", :type "text"}}
+   :params        ["2" "2"]}
+  (:native (expand-params {:driver     (driver/engine->driver :h2)
+                           :native     {:query         "SELECT * FROM ORDERS WHERE true [[ AND ID = {{id}} OR USER_ID = {{id}} ]]"
+                                        :template_tags {:id {:name "id", :display_name "ID", :type "text"}}}
+                           :parameters [{:type "category", :target ["variable" ["template-tag" "id"]], :value "2"}]})))
diff --git a/test/metabase/query_processor/util_test.clj b/test/metabase/query_processor/util_test.clj
index 160278cdc2f1852da20ac0c35c5eda8b99d69a4e..357583558a259029c48d0101d354c459d7c69542 100644
--- a/test/metabase/query_processor/util_test.clj
+++ b/test/metabase/query_processor/util_test.clj
@@ -94,3 +94,14 @@
     (qputil/query-hash {:query :abc})
     (qputil/query-hash {:query :abc, :constraints nil})
     (qputil/query-hash {:query :abc, :constraints {}})))
+
+;; make sure two different natiev queries have different hashes!
+(expect
+  false
+  (array=
+    (qputil/query-hash {:database    2
+                        :type        "native"
+                        :native      {:query "SELECT pg_sleep(15), 1 AS one"}})
+    (qputil/query-hash {:database    2
+                        :type        "native"
+                        :native      {:query "SELECT pg_sleep(15), 2 AS two"}})))
diff --git a/test/metabase/query_processor_test.clj b/test/metabase/query_processor_test.clj
index 04989644a6e345eb4e400ba9936cf784657e10bd..3e0fb4f0391f77122d15acba55ae9645e5121b12 100644
--- a/test/metabase/query_processor_test.clj
+++ b/test/metabase/query_processor_test.clj
@@ -294,7 +294,7 @@
   {:style/indent 0}
   [results]
   (vec (or (get-in results [:data :rows])
-           (println (u/pprint-to-str 'red results))
+           (println (u/pprint-to-str 'red results)) ; DEBUG
            (throw (Exception. "Error!")))))
 
 (defn rows+column-names
diff --git a/test/metabase/query_processor_test/aggregation_test.clj b/test/metabase/query_processor_test/aggregation_test.clj
index 759d5e26e57a62c485d473a0a5534ccaf22ad3fd..d6b8267866176ce7e2bc01ae7810ebcb0366b60f 100644
--- a/test/metabase/query_processor_test/aggregation_test.clj
+++ b/test/metabase/query_processor_test/aggregation_test.clj
@@ -149,8 +149,8 @@
             (ql/aggregation (ql/avg $price) (ql/count) (ql/sum $price))))))
 
 ;; make sure that multiple aggregations of the same type have the correct metadata (#4003)
-;; (TODO - this isn't tested against Mongo or BigQuery because those drivers don't currently work correctly with multiple columns with the same name)
-(datasets/expect-with-engines (disj non-timeseries-engines :mongo :bigquery)
+;; (TODO - this isn't tested against Mongo, BigQuery or Presto because those drivers don't currently work correctly with multiple columns with the same name)
+(datasets/expect-with-engines (disj non-timeseries-engines :mongo :bigquery :presto)
   [(aggregate-col :count)
    (assoc (aggregate-col :count)
      :display_name    "count_2"
diff --git a/test/metabase/query_processor_test/date_bucketing_test.clj b/test/metabase/query_processor_test/date_bucketing_test.clj
index eeee25db76c9fc3660e461a2f403d29bcd9cb6a5..603e8d93057fb3fdafc4cd9473181f0a7b6fbef5 100644
--- a/test/metabase/query_processor_test/date_bucketing_test.clj
+++ b/test/metabase/query_processor_test/date_bucketing_test.clj
@@ -37,7 +37,7 @@
      ["2015-06-02 08:20:00" 1]
      ["2015-06-02 11:11:00" 1]]
 
-    (contains? #{:redshift :sqlserver :bigquery :mongo :postgres :vertica :h2 :oracle} *engine*)
+    (contains? #{:redshift :sqlserver :bigquery :mongo :postgres :vertica :h2 :oracle :presto} *engine*)
     [["2015-06-01T10:31:00.000Z" 1]
      ["2015-06-01T16:06:00.000Z" 1]
      ["2015-06-01T17:23:00.000Z" 1]
@@ -246,7 +246,7 @@
     (contains? #{:sqlserver :sqlite :crate :oracle} *engine*)
     [[23 54] [24 46] [25 39] [26 61]]
 
-    (contains? #{:mongo :redshift :bigquery :postgres :vertica :h2} *engine*)
+    (contains? #{:mongo :redshift :bigquery :postgres :vertica :h2 :presto} *engine*)
     [[23 46] [24 47] [25 40] [26 60] [27 7]]
 
     :else
diff --git a/test/metabase/sync_database_test.clj b/test/metabase/sync_database_test.clj
index 4b1a7e2e0c3fd69028b472c1aec8108068e24aec..d44d2967019187c16c3f7692855d908b6abc442e 100644
--- a/test/metabase/sync_database_test.clj
+++ b/test/metabase/sync_database_test.clj
@@ -1,17 +1,23 @@
 (ns metabase.sync-database-test
-  (:require [expectations :refer :all]
+  (:require [clojure.java.jdbc :as jdbc]
+            [clojure.string :as str]
+            [expectations :refer :all]
             (toucan [db :as db]
                     [hydrate :refer [hydrate]])
             [toucan.util.test :as tt]
-            [metabase.driver :as driver]
+            (metabase [db :as mdb]
+                      [driver :as driver])
+            [metabase.driver.generic-sql :as sql]
             (metabase.models [database :refer [Database]]
                              [field :refer [Field]]
                              [field-values :refer [FieldValues]]
                              [raw-table :refer [RawTable]]
                              [table :refer [Table]])
             [metabase.sync-database :refer :all]
-            (metabase.test [data :refer :all]
-                           [util :refer [resolve-private-vars] :as tu])
+            metabase.sync-database.analyze
+            [metabase.test.data :refer :all]
+            [metabase.test.data.interface :as i]
+            [metabase.test.util :refer [resolve-private-vars] :as tu]
             [metabase.util :as u]))
 
 (def ^:private ^:const sync-test-tables
@@ -317,3 +323,33 @@
      ;; 3. Now re-sync the table and make sure the value is back
      (do (sync-table! @venues-table)
          (get-field-values))]))
+
+
+;;; -------------------- Make sure that if a Field's cardinality passes `metabase.sync-database.analyze/low-cardinality-threshold` (currently 300) (#3215) --------------------
+(defn- insert-range-sql [rang]
+  (str "INSERT INTO blueberries_consumed (num) VALUES "
+       (str/join ", " (for [n rang]
+                        (str "(" n ")")))))
+
+(expect
+  false
+  (let [details {:db (str "mem:" (tu/random-name) ";DB_CLOSE_DELAY=10")}]
+    (binding [mdb/*allow-potentailly-unsafe-connections* true]
+      (tt/with-temp Database [db {:engine :h2, :details details}]
+        (let [driver (driver/engine->driver :h2)
+              spec   (sql/connection-details->spec driver details)
+              exec!  #(doseq [statement %]
+                        (jdbc/execute! spec [statement]))]
+          ;; create the `blueberries_consumed` table and insert a 100 values
+          (exec! ["CREATE TABLE blueberries_consumed (num INTEGER NOT NULL);"
+                  (insert-range-sql (range 100))])
+          (sync-database! db, :full-sync? true)
+          (let [table-id (db/select-one-id Table :db_id (u/get-id db))
+                field-id (db/select-one-id Field :table_id table-id)]
+            ;; field values should exist...
+            (assert (= (count (db/select-one-field :values FieldValues :field_id field-id))
+                       100))
+            ;; ok, now insert enough rows to push the field past the `low-cardinality-threshold` and sync again, there should be no more field values
+            (exec! [(insert-range-sql (range 100 (+ 100 @(resolve 'metabase.sync-database.analyze/low-cardinality-threshold))))])
+            (sync-database! db, :full-sync? true)
+            (db/exists? FieldValues :field_id field-id)))))))
diff --git a/test/metabase/test/data/druid.clj b/test/metabase/test/data/druid.clj
index 0bde54cd1d0d6ef3d529d1af351b8e00e0a10f75..0f24351c182126adabbbb5461eb799b8bf687d45 100644
--- a/test/metabase/test/data/druid.clj
+++ b/test/metabase/test/data/druid.clj
@@ -111,7 +111,7 @@
     (println "STATUS URL: " (str indexer-endpoint "/" task "/status"))
     (loop [remaining-seconds indexer-timeout-seconds]
       (let [status (get-in (GET status-url) [:status :status])]
-        (println (format "%s (%d seconds elapsed)" status (- indexer-timeout-seconds remaining-seconds)))
+        (printf "%s (%d seconds elapsed)\n" status (- indexer-timeout-seconds remaining-seconds))
         (when (not= status "SUCCESS")
           (when (<= remaining-seconds 0)
             (throw (Exception. (str "Failed to finish indexing druid data after " indexer-timeout-seconds " seconds!"))))
diff --git a/test/metabase/test/data/generic_sql.clj b/test/metabase/test/data/generic_sql.clj
index 5a93ba82eb14fe52b3ceb367b61a30b36f82f0cb..49a3b4fe11ae5f064e3f9f7ccb691e58422c2611 100644
--- a/test/metabase/test/data/generic_sql.clj
+++ b/test/metabase/test/data/generic_sql.clj
@@ -13,7 +13,8 @@
                                 [interface :as i])
             [metabase.util :as u]
             [metabase.util.honeysql-extensions :as hx])
-  (:import clojure.lang.Keyword
+  (:import java.sql.SQLException
+           clojure.lang.Keyword
            (metabase.test.data.interface DatabaseDefinition
                                          FieldDefinition
                                          TableDefinition)))
@@ -218,7 +219,7 @@
                                           :quoting             (sql/quote-style driver)
                                           :allow-dashed-names? true)))]
     (try (jdbc/execute! spec sql+args)
-         (catch java.sql.SQLException e
+         (catch SQLException e
            (println (u/format-color 'red "INSERT FAILED: \n%s\n" sql+args))
            (jdbc/print-sql-exception-chain e)))))
 
@@ -249,15 +250,15 @@
       (let [sql (s/replace sql #";+" ";")]
         (try
           (jdbc/execute! (database->spec driver context dbdef) [sql] {:transaction? false, :multi? true})
-          (catch java.sql.SQLException e
+          (catch SQLException e
             (println "Error executing SQL:" sql)
-            (println (format "Caught SQLException:\n%s"
-                             (with-out-str (jdbc/print-sql-exception-chain e))))
+            (printf "Caught SQLException:\n%s\n"
+                    (with-out-str (jdbc/print-sql-exception-chain e)))
             (throw e))
           (catch Throwable e
             (println "Error executing SQL:" sql)
-            (println (format "Caught Exception: %s %s\n%s" (class e) (.getMessage e)
-                             (with-out-str (.printStackTrace e))))
+            (printf "Caught Exception: %s %s\n%s\n" (class e) (.getMessage e)
+                    (with-out-str (.printStackTrace e)))
             (throw e)))))))
 
 
diff --git a/test/metabase/test/data/interface.clj b/test/metabase/test/data/interface.clj
index af3208d855d94c77711e16efbce683abcf0709fc..f460946e7dac64caedca25ce1ad18eef7cc67eb0 100644
--- a/test/metabase/test/data/interface.clj
+++ b/test/metabase/test/data/interface.clj
@@ -30,8 +30,9 @@
 
 (defn escaped-name
   "Return escaped version of database name suitable for use as a filename / database name / etc."
-  ^String [^DatabaseDefinition database-definition]
-  (str/replace (:database-name database-definition) #"\s+" "_"))
+  ^String [^DatabaseDefinition {:keys [database-name]}]
+  {:pre [(string? database-name)]}
+  (str/replace database-name #"\s+" "_"))
 
 (defn db-qualified-table-name
   "Return a combined table name qualified with the name of its database, suitable for use as an identifier.
diff --git a/test/metabase/test/data/presto.clj b/test/metabase/test/data/presto.clj
new file mode 100644
index 0000000000000000000000000000000000000000..790907b1abf8aef6f032d5bdc710f1c8ee235c7c
--- /dev/null
+++ b/test/metabase/test/data/presto.clj
@@ -0,0 +1,107 @@
+(ns metabase.test.data.presto
+  (:require [clojure.string :as s]
+            [environ.core :refer [env]]
+            (honeysql [core :as hsql]
+                      [helpers :as h])
+            [metabase.driver.generic-sql.util.unprepare :as unprepare]
+            [metabase.test.data.interface :as i]
+            [metabase.test.util :refer [resolve-private-vars]]
+            [metabase.util :as u]
+            [metabase.util.honeysql-extensions :as hx])
+  (:import java.util.Date
+           metabase.driver.presto.PrestoDriver
+           (metabase.query_processor.interface DateTimeValue Value)))
+
+(resolve-private-vars metabase.driver.presto execute-presto-query! presto-type->base-type quote-name quote+combine-names)
+
+;;; Helpers
+
+(defn- get-env-var [env-var]
+  (or (env (keyword (format "mb-presto-%s" (name env-var))))
+      (throw (Exception. (format "In order to test Presto, you must specify the env var MB_PRESTO_%s."
+                                 (s/upper-case (s/replace (name env-var) #"-" "_")))))))
+
+
+;;; IDatasetLoader implementation
+
+(defn- database->connection-details [context {:keys [database-name]}]
+  (merge {:host (get-env-var :host)
+          :port (get-env-var :port)
+          :user "metabase"
+          :ssl  false}
+         (when (= context :db)
+           {:catalog database-name})))
+
+(defn- qualify-name
+  ;; we have to use the default schema from the in-memory connectory
+  ([db-name]                       [db-name])
+  ([db-name table-name]            [db-name "default" table-name])
+  ([db-name table-name field-name] [db-name "default" table-name field-name]))
+
+(defn- qualify+quote-name [& names]
+  (apply quote+combine-names (apply qualify-name names)))
+
+(defn- field-base-type->dummy-value [field-type]
+  ;; we need a dummy value for every base-type to make a properly typed SELECT statement
+  (if (keyword? field-type)
+    (case field-type
+      :type/Boolean    "TRUE"
+      :type/Integer    "1"
+      :type/BigInteger "cast(1 AS bigint)"
+      :type/Float      "1.0"
+      :type/Decimal    "DECIMAL '1.0'"
+      :type/Text       "cast('' AS varchar(255))"
+      :type/Date       "current_timestamp" ; this should probably be a date type, but the test data begs to differ
+      :type/DateTime   "current_timestamp"
+      "from_hex('00')") ; this might not be the best default ever
+    ;; we were given a native type, map it back to a base-type and try again
+    (field-base-type->dummy-value (presto-type->base-type field-type))))
+
+(defn- create-table-sql [{:keys [database-name]} {:keys [table-name], :as tabledef}]
+  (let [field-definitions (conj (:field-definitions tabledef) {:field-name "id", :base-type  :type/Integer})
+        dummy-values      (map (comp field-base-type->dummy-value :base-type) field-definitions)
+        columns           (map :field-name field-definitions)]
+    ;; Presto won't let us use the `CREATE TABLE (...)` form, but we can still do it creatively if we select the right types out of thin air
+    (format "CREATE TABLE %s AS SELECT * FROM (VALUES (%s)) AS t (%s) WHERE 1 = 0"
+            (qualify+quote-name database-name table-name)
+            (s/join \, dummy-values)
+            (s/join \, (map quote-name columns)))))
+
+(defn- drop-table-if-exists-sql [{:keys [database-name]} {:keys [table-name]}]
+  (str "DROP TABLE IF EXISTS " (qualify+quote-name database-name table-name)))
+
+(defn- insert-sql [{:keys [database-name]} {:keys [table-name], :as tabledef} rows]
+  (let [field-definitions (conj (:field-definitions tabledef) {:field-name "id"})
+        columns           (map (comp keyword :field-name) field-definitions)
+        [query & params]  (-> (apply h/columns columns)
+                              (h/insert-into (apply hsql/qualify (qualify-name database-name table-name)))
+                              (h/values rows)
+                              (hsql/format :allow-dashed-names? true, :quoting :ansi))]
+    (if (nil? params)
+      query
+      (unprepare/unprepare (cons query params) :quote-escape "'", :iso-8601-fn :from_iso8601_timestamp))))
+
+(defn- create-db! [{:keys [table-definitions] :as dbdef}]
+  (let [details (database->connection-details :db dbdef)]
+    (doseq [tabledef table-definitions
+            :let [rows       (:rows tabledef)
+                  keyed-rows (map-indexed (fn [i row] (conj row (inc i))) rows) ; generate an ID for each row because we don't have auto increments
+                  batches    (partition 100 100 nil keyed-rows)]]               ; make 100 rows batches since we have to inline everything
+      (execute-presto-query! details (drop-table-if-exists-sql dbdef tabledef))
+      (execute-presto-query! details (create-table-sql dbdef tabledef))
+      (doseq [batch batches]
+        (execute-presto-query! details (insert-sql dbdef tabledef batch))))))
+
+
+;;; IDatasetLoader implementation
+
+(u/strict-extend PrestoDriver
+  i/IDatasetLoader
+  (merge i/IDatasetLoaderDefaultsMixin
+         {:engine                             (constantly :presto)
+          :database->connection-details       (u/drop-first-arg database->connection-details)
+          :create-db!                         (u/drop-first-arg create-db!)
+          :default-schema                     (constantly "default")
+          :format-name                        (u/drop-first-arg s/lower-case)
+          ;; FIXME Presto actually has very good timezone support
+          :has-questionable-timezone-support? (constantly true)}))
diff --git a/test/metabase/test/data/sqlserver.clj b/test/metabase/test/data/sqlserver.clj
index 858ebdac8104c515b51a290010c551413c59999b..d4bb90ec570292308bc1cf8edddfa39f86105052 100644
--- a/test/metabase/test/data/sqlserver.clj
+++ b/test/metabase/test/data/sqlserver.clj
@@ -102,7 +102,7 @@
       (with-redefs [+suffix identity]
         (doseq [db leftover-dbs]
           (u/ignore-exceptions
-            (println (format "Deleting leftover SQL Server DB '%s'..." db))
+            (printf "Deleting leftover SQL Server DB '%s'...\n" db)
             ;; Don't try to kill other connections to this DB with SET SINGLE_USER -- some other instance (eg CI) might be using it
             (jdbc/execute! connection-spec [(format "DROP DATABASE \"%s\";" db)])
             (println "[ok]")))))))
diff --git a/test/metabase/test/data/users.clj b/test/metabase/test/data/users.clj
index 2c13813a3a1e057fd71c0af4078d9285f363dcd6..5698ebae330aae5620c9422b05d3653aaa14eb40 100644
--- a/test/metabase/test/data/users.clj
+++ b/test/metabase/test/data/users.clj
@@ -3,13 +3,14 @@
   ;; TODO - maybe this namespace should just be `metabase.test.users`.
   (:require [medley.core :as m]
             [toucan.db :as db]
-            (metabase [config :as config]
-                      [core :as core])
+            [metabase.config :as config]
+            [metabase.core.initialization-status :as init-status]
             [metabase.http-client :as http]
             (metabase.models [permissions-group :as perms-group]
                              [user :refer [User]])
             [metabase.util :as u]
-            [metabase.test.util :refer [random-name]]))
+            [metabase.test.util :refer [random-name]])
+  (:import clojure.lang.ExceptionInfo))
 
 ;;; ------------------------------------------------------------ User Definitions ------------------------------------------------------------
 
@@ -56,10 +57,10 @@
    ;; only need to wait when running unit tests. When doing REPL dev and using the test users we're probably
    ;; the server is probably a separate process (`lein ring server`)
    (when config/is-test?
-     (when-not (core/initialized?)
+     (when-not (init-status/complete?)
        (when (<= max-wait-seconds 0)
          (throw (Exception. "Metabase still hasn't finished initializing.")))
-       (println (format "Metabase is not yet initialized, waiting 1 second (max wait remaining: %d seconds)..." max-wait-seconds))
+       (printf "Metabase is not yet initialized, waiting 1 second (max wait remaining: %d seconds)...\n" max-wait-seconds)
        (Thread/sleep 1000)
        (recur (dec max-wait-seconds))))))
 
@@ -71,6 +72,7 @@
   {:pre [(string? email) (string? first) (string? last) (string? password) (m/boolean? superuser) (m/boolean? active)]}
   (wait-for-initiailization)
   (or (User :email email)
+      (println "Creating test user:" email) ; DEBUG
       (db/insert! User
         :email        email
         :first_name   first
@@ -123,7 +125,7 @@
     (fn [id]
       (@m id))))
 
-(def ^:private tokens (atom {}))
+(defonce ^:private tokens (atom {}))
 
 (defn- username->token [username]
   (or (@tokens username)
@@ -134,18 +136,15 @@
 (defn- client-fn [username & args]
   (try
     (apply http/client (username->token username) args)
-    (catch Throwable e
+    (catch ExceptionInfo e
       (let [{:keys [status-code]} (ex-data e)]
         (when-not (= status-code 401)
           (throw e))
         ;; If we got a 401 unauthenticated clear the tokens cache + recur
+        (printf "Got 401 (Unauthenticated) for %s. Clearing cached auth tokens and retrying request.\n" username) ; DEBUG
         (reset! tokens {})
         (apply client-fn username args)))))
 
-;; TODO - does it make sense just to make this a non-higher-order function? Or a group of functions, e.g.
-;; (GET :rasta 200 "field/10/values")
-;; vs.
-;; ((user->client :rasta) :get 200 "field/10/values")
 (defn user->client
   "Returns a `metabase.http-client/client` partially bound with the credentials for User with USERNAME.
    In addition, it forces lazy creation of the User if needed.
@@ -160,4 +159,4 @@
   "Delete all users besides the 4 persistent test users.
    This is a HACK to work around tests that don't properly clean up after themselves; one day we should be able to remove this. (TODO)"
   []
-  (db/delete! 'User :id [:not-in (map user->id [:crowberto :lucky :rasta :trashbird])]))
+  (db/delete! User :id [:not-in (map user->id [:crowberto :lucky :rasta :trashbird])]))
diff --git a/test/metabase/test_setup.clj b/test/metabase/test_setup.clj
index 7d81cc710acee50e5324d72c96932d85e36f3d62..3f9b447793cb1f942341abdf1fe485a909ac64de 100644
--- a/test/metabase/test_setup.clj
+++ b/test/metabase/test_setup.clj
@@ -5,8 +5,9 @@
             [clojure.set :as set]
             [clojure.tools.logging :as log]
             [expectations :refer :all]
-            (metabase [core :as core]
-                      [db :as mdb]
+            [metabase.core :as core]
+            [metabase.core.initialization-status :as init-status]
+            (metabase [db :as mdb]
                       [driver :as driver])
             (metabase.models [setting :as setting]
                              [table :refer [Table]])
@@ -79,7 +80,7 @@
       (log/info (format "Setting up %s test DB and running migrations..." (name (mdb/db-type))))
       (mdb/setup-db! :auto-migrate true)
       (setting/set! :site-name "Metabase Test")
-      (core/initialization-complete!)
+      (init-status/set-complete!)
 
       ;; make sure the driver test extensions are loaded before running the tests. :reload them because otherwise we get wacky 'method in protocol not implemented' errors
       ;; when running tests against an individual namespace
diff --git a/test/metabase/timeseries_query_processor_test.clj b/test/metabase/timeseries_query_processor_test.clj
index 5d05675e8e36c968b57db2d0015acc32577c06ec..81f48c2ed9db80654346434781a68d95975b2529 100644
--- a/test/metabase/timeseries_query_processor_test.clj
+++ b/test/metabase/timeseries_query_processor_test.clj
@@ -45,7 +45,7 @@
 
 (defn- data [results]
   (when-let [data (or (:data results)
-                      (println (u/pprint-to-str results)))]
+                      (println (u/pprint-to-str results)))] ; DEBUG
     (-> data
         (select-keys [:columns :rows])
         (update :rows vec))))
diff --git a/test/metabase/util_test.clj b/test/metabase/util_test.clj
index a17d043795dad96442589aaabc2451c7c9f6fe03..8520c9866e9978d35e3ba369980f980ee988d8ef 100644
--- a/test/metabase/util_test.clj
+++ b/test/metabase/util_test.clj
@@ -204,9 +204,29 @@
   (select-nested-keys {} [:c]))
 
 
-;; tests for base-64-string?
+;;; tests for base-64-string?
 (expect (base-64-string? "ABc"))
 (expect (base-64-string? "ABc/+asdasd=="))
 (expect false (base-64-string? 100))
 (expect false (base-64-string? "<<>>"))
 (expect false (base-64-string? "{\"a\": 10}"))
+
+
+;;; tests for `occurances-of-substring`
+
+;; return nil if one or both strings are nil or empty
+(expect nil (occurances-of-substring nil   nil))
+(expect nil (occurances-of-substring nil   ""))
+(expect nil (occurances-of-substring ""    nil))
+(expect nil (occurances-of-substring ""    ""))
+(expect nil (occurances-of-substring "ABC" ""))
+(expect nil (occurances-of-substring "" "  ABC"))
+
+(expect 1 (occurances-of-substring "ABC" "A"))
+(expect 2 (occurances-of-substring "ABA" "A"))
+(expect 3 (occurances-of-substring "AAA" "A"))
+
+(expect 0 (occurances-of-substring "ABC"                                                                               "{{id}}"))
+(expect 1 (occurances-of-substring "WHERE ID = {{id}}"                                                                 "{{id}}"))
+(expect 2 (occurances-of-substring "WHERE ID = {{id}} OR USER_ID = {{id}}"                                             "{{id}}"))
+(expect 3 (occurances-of-substring "WHERE ID = {{id}} OR USER_ID = {{id}} OR TOUCAN_ID = {{id}} OR BIRD_ID = {{bird}}" "{{id}}"))
diff --git a/webpack.config.js b/webpack.config.js
index 6183bf73437d7e3d01f62dc603001ac35541fb55..ee560eb282106139e925d4155bb37dc81732a63a 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -9,6 +9,7 @@ var webpackPostcssTools = require('webpack-postcss-tools');
 
 var ExtractTextPlugin = require('extract-text-webpack-plugin');
 var HtmlWebpackPlugin = require('html-webpack-plugin');
+var HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');
 var UnusedFilesWebpackPlugin = require("unused-files-webpack-plugin").default;
 var BannerWebpackPlugin = require('banner-webpack-plugin');
 
@@ -27,18 +28,16 @@ function hasArg(arg) {
 var SRC_PATH = __dirname + '/frontend/src/metabase';
 var BUILD_PATH = __dirname + '/resources/frontend_client';
 
+// default NODE_ENV to development
+var NODE_ENV = process.env["NODE_ENV"] || "development";
 
 // Need to scan the CSS files for variable and custom media used across files
 // NOTE: this requires "webpack -w" (watch mode) to be restarted when variables change :(
-var isWatching = hasArg("-w") || hasArg("--watch");
-if (isWatching) {
-    console.warn("Warning: in webpack watch mode you must restart webpack if you change any CSS variables or custom media queries");
+var IS_WATCHING = hasArg("-w") || hasArg("--watch");
+if (IS_WATCHING) {
+    process.stderr.write("Warning: in webpack watch mode you must restart webpack if you change any CSS variables or custom media queries\n");
 }
 
-// default NODE_ENV to development
-var NODE_ENV = process.env["NODE_ENV"] || "development";
-process.stderr.write("webpack env: " + NODE_ENV + "\n");
-
 // Babel:
 var BABEL_CONFIG = {
     cacheDirectory: ".babel_cache"
@@ -159,19 +158,25 @@ var config = module.exports = {
             filename: '../../index.html',
             chunks: ["app-main", "styles"],
             template: __dirname + '/resources/frontend_client/index_template.html',
-            inject: 'head'
+            inject: 'head',
+            alwaysWriteToDisk: true,
         }),
         new HtmlWebpackPlugin({
             filename: '../../public.html',
             chunks: ["app-public", "styles"],
             template: __dirname + '/resources/frontend_client/index_template.html',
-            inject: 'head'
+            inject: 'head',
+            alwaysWriteToDisk: true,
         }),
         new HtmlWebpackPlugin({
             filename: '../../embed.html',
             chunks: ["app-embed", "styles"],
             template: __dirname + '/resources/frontend_client/index_template.html',
-            inject: 'head'
+            inject: 'head',
+            alwaysWriteToDisk: true,
+        }),
+        new HtmlWebpackHarddiskPlugin({
+            outputPath: __dirname + '/resources/frontend_client/app/dist'
         }),
         new webpack.DefinePlugin({
             'process.env': {
diff --git a/yarn.lock b/yarn.lock
index 862aac640974ad0e985bf73a603ec46f027e7d48..a343edb47001c17cfd759be5ee61eccd6f0f3c6e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -218,7 +218,7 @@ annotate-react-dom@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/annotate-react-dom/-/annotate-react-dom-1.1.0.tgz#607c14d2565198d4bf365f6f05c60a61ba939a16"
 
-ansi-escapes@^1.1.0, ansi-escapes@^1.4.0:
+ansi-escapes@^1.0.0, ansi-escapes@^1.1.0, ansi-escapes@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
 
@@ -248,6 +248,12 @@ ansi-styles@^2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
 
+ansi-styles@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.0.0.tgz#5404e93a544c4fec7f048262977bebfe3155e0c1"
+  dependencies:
+    color-convert "^1.0.0"
+
 ansicolors@~0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef"
@@ -267,6 +273,10 @@ anymatch@^1.3.0:
     arrify "^1.0.0"
     micromatch "^2.1.5"
 
+app-root-path@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.0.1.tgz#cd62dcf8e4fd5a417efc664d2e5b10653c651b46"
+
 append-transform@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991"
@@ -390,6 +400,14 @@ assert@^1.1.1:
   dependencies:
     util "0.10.3"
 
+ast-types@0.8.18:
+  version "0.8.18"
+  resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.8.18.tgz#c8b98574898e8914e9d8de74b947564a9fe929af"
+
+ast-types@0.9.4:
+  version "0.9.4"
+  resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.4.tgz#410d1f81890aeb8e0a38621558ba5869ae53c91b"
+
 ast-types@0.9.5:
   version "0.9.5"
   resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.5.tgz#1a660a09945dbceb1f9c9cbb715002617424e04a"
@@ -464,7 +482,7 @@ babel-cli@^6.11.4:
   optionalDependencies:
     chokidar "^1.6.1"
 
-babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0:
+babel-code-frame@6.22.0, babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0:
   version "6.22.0"
   resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4"
   dependencies:
@@ -1370,7 +1388,7 @@ babel-types@^6.15.0, babel-types@^6.16.0, babel-types@^6.18.0, babel-types@^6.19
     lodash "^4.2.0"
     to-fast-properties "^1.0.1"
 
-babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0:
+babylon@6.15.0, babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0:
   version "6.15.0"
   resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e"
 
@@ -1708,7 +1726,7 @@ chalk@0.5.1:
     strip-ansi "^0.3.0"
     supports-color "^0.2.0"
 
-chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
+chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
   dependencies:
@@ -1801,18 +1819,29 @@ cli-color@^0.3.2:
     memoizee "~0.3.8"
     timers-ext "0.1"
 
-cli-cursor@^1.0.1:
+cli-cursor@^1.0.1, cli-cursor@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
   dependencies:
     restore-cursor "^1.0.1"
 
+cli-spinners@^0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c"
+
 cli-table@^0.3.1:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23"
   dependencies:
     colors "1.0.3"
 
+cli-truncate@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574"
+  dependencies:
+    slice-ansi "0.0.4"
+    string-width "^1.0.1"
+
 cli-usage@^0.1.1:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/cli-usage/-/cli-usage-0.1.4.tgz#7c01e0dc706c234b39c933838c8e20b2175776e2"
@@ -1862,7 +1891,7 @@ color-convert@^0.5.3:
   version "0.5.3"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd"
 
-color-convert@^1.3.0, color-convert@^1.8.2:
+color-convert@^1.0.0, color-convert@^1.3.0, color-convert@^1.8.2:
   version "1.9.0"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
   dependencies:
@@ -1919,7 +1948,7 @@ colors@1.0.3, colors@1.0.x:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
 
-colors@1.1.2, colors@^1.1.0, colors@~1.1.2:
+colors@1.1.2, colors@>=0.6.2, colors@^1.1.0, colors@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
 
@@ -2100,6 +2129,19 @@ core-util-is@~1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
 
+cosmiconfig@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-1.1.0.tgz#0dea0f9804efdfb929fbb1b188e25553ea053d37"
+  dependencies:
+    graceful-fs "^4.1.2"
+    js-yaml "^3.4.3"
+    minimist "^1.2.0"
+    object-assign "^4.0.1"
+    os-homedir "^1.0.1"
+    parse-json "^2.2.0"
+    pinkie-promise "^2.0.0"
+    require-from-string "^1.1.0"
+
 cosmiconfig@^2.1.0, cosmiconfig@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.1.1.tgz#817f2c2039347a1e9bf7d090c0923e53f749ca82"
@@ -2141,6 +2183,14 @@ cross-spawn-async@^1.0.1:
     lru-cache "^2.6.5"
     which "^1.1.1"
 
+cross-spawn@^5.0.1:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
+  dependencies:
+    lru-cache "^4.0.1"
+    shebang-command "^1.2.0"
+    which "^1.2.9"
+
 crossfilter2@~1.3:
   version "1.3.14"
   resolved "https://registry.yarnpkg.com/crossfilter2/-/crossfilter2-1.3.14.tgz#c45bd8d335f6c91accbac26eda203377f195f680"
@@ -2307,6 +2357,12 @@ custom-event@~1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
 
+cxs@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/cxs/-/cxs-3.0.4.tgz#2e1a1537742931a53dbe3157afbf121da62df797"
+  dependencies:
+    glamor "^2.17.14"
+
 cycle@1.0.x:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2"
@@ -2327,7 +2383,7 @@ dashdash@^1.12.0:
   dependencies:
     assert-plus "^1.0.0"
 
-date-fns@^1.23.0:
+date-fns@^1.23.0, date-fns@^1.27.2:
   version "1.27.2"
   resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.27.2.tgz#ce82f420bc028356cc661fc55c0494a56a990c9c"
 
@@ -2570,6 +2626,10 @@ electron-to-chromium@^1.2.3:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.2.3.tgz#4b4d04d237c301f72e2d15c2137b2b79f9f5ab76"
 
+elegant-spinner@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
+
 element-class@^0.2.0:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/element-class/-/element-class-0.2.2.tgz#9d3bbd0767f9013ef8e1c8ebe722c1402a60050e"
@@ -2967,7 +3027,7 @@ estraverse@~4.1.0:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2"
 
-esutils@^2.0.0, esutils@^2.0.2:
+esutils@2.0.2, esutils@^2.0.0, esutils@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
 
@@ -3012,6 +3072,18 @@ exec-sh@^0.2.0:
   dependencies:
     merge "^1.1.3"
 
+execa@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-0.6.0.tgz#934fc9f04a9febb4d4b449d976e92cfd95ef4f6e"
+  dependencies:
+    cross-spawn "^5.0.1"
+    get-stream "^3.0.0"
+    is-stream "^1.1.0"
+    npm-run-path "^2.0.0"
+    p-finally "^1.0.0"
+    signal-exit "^3.0.0"
+    strip-eof "^1.0.0"
+
 exenv@1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.0.tgz#3835f127abf075bfe082d0aed4484057c78e3c89"
@@ -3141,7 +3213,7 @@ fb-watchman@^1.8.0, fb-watchman@^1.9.0:
   dependencies:
     bser "1.0.2"
 
-fbjs@^0.8.1, fbjs@^0.8.4:
+fbjs@^0.8.1, fbjs@^0.8.4, fbjs@^0.8.8:
   version "0.8.9"
   resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.9.tgz#180247fbd347dcc9004517b904f865400a0c8f14"
   dependencies:
@@ -3153,7 +3225,7 @@ fbjs@^0.8.1, fbjs@^0.8.4:
     setimmediate "^1.0.5"
     ua-parser-js "^0.7.9"
 
-figures@^1.3.5:
+figures@^1.3.5, figures@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
   dependencies:
@@ -3224,6 +3296,10 @@ find-cache-dir@^0.1.1:
     mkdirp "^0.5.1"
     pkg-dir "^1.0.0"
 
+find-parent-dir@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54"
+
 find-root@^0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/find-root/-/find-root-0.1.2.tgz#98d2267cff1916ccaf2743b3a0eea81d79d7dcd1"
@@ -3252,6 +3328,14 @@ flow-bin@^0.37.4:
   version "0.37.4"
   resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.37.4.tgz#3d8da2ef746e80e730d166e09040f4198969b76b"
 
+flow-parser@0.40.0:
+  version "0.40.0"
+  resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.40.0.tgz#b3444742189093323c4319c4fe9d35391f46bcbc"
+  dependencies:
+    ast-types "0.8.18"
+    colors ">=0.6.2"
+    minimist ">=0.2.0"
+
 flux-standard-action@^0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/flux-standard-action/-/flux-standard-action-0.6.1.tgz#6f34211b94834ea1c3cc30f4e7afad3d0fbf71a2"
@@ -3411,12 +3495,28 @@ get-caller-file@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
 
+get-stdin@5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398"
+
+get-stream@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
+
 getpass@^0.1.1:
   version "0.1.6"
   resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.6.tgz#283ffd9fc1256840875311c1b60e8c40187110e6"
   dependencies:
     assert-plus "^1.0.0"
 
+glamor@^2.17.14:
+  version "2.20.24"
+  resolved "https://registry.yarnpkg.com/glamor/-/glamor-2.20.24.tgz#a299af2eec687322634ba38e4a0854d8743d2041"
+  dependencies:
+    babel-runtime "^6.18.0"
+    fbjs "^0.8.8"
+    object-assign "^4.1.0"
+
 glob-base@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -3430,7 +3530,7 @@ glob-parent@^2.0.0:
   dependencies:
     is-glob "^2.0.0"
 
-glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1:
+glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1:
   version "7.1.1"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
   dependencies:
@@ -3623,6 +3723,12 @@ html-minifier@^3.2.3:
     relateurl "0.2.x"
     uglify-js "2.7.x"
 
+html-webpack-harddisk-plugin@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/html-webpack-harddisk-plugin/-/html-webpack-harddisk-plugin-0.1.0.tgz#432024961a21ac668fa2b5dfe24629c60b9c58d7"
+  dependencies:
+    mkdirp "^0.5.1"
+
 html-webpack-plugin@^2.14.0:
   version "2.28.0"
   resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-2.28.0.tgz#2e7863b57e5fd48fe263303e2ffc934c3064d009"
@@ -3702,6 +3808,15 @@ humanize-plus@^1.8.1:
   version "1.8.2"
   resolved "https://registry.yarnpkg.com/humanize-plus/-/humanize-plus-1.8.2.tgz#a65b34459ad6367adbb3707a82a3c9f916167030"
 
+husky@^0.13.2:
+  version "0.13.2"
+  resolved "https://registry.yarnpkg.com/husky/-/husky-0.13.2.tgz#9dcf212f88e61dba36f17be1a202ed61ff6c0661"
+  dependencies:
+    chalk "^1.1.3"
+    find-parent-dir "^0.3.0"
+    is-ci "^1.0.9"
+    normalize-path "^1.0.0"
+
 icepick@^1.1.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/icepick/-/icepick-1.3.0.tgz#e4942842ed8f9ee778d7dd78f7e36627f49fdaef"
@@ -3762,6 +3877,16 @@ imurmurhash@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
 
+indent-string@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
+  dependencies:
+    repeating "^2.0.0"
+
+indent-string@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.1.0.tgz#08ff4334603388399b329e6b9538dc7a3cf5de7d"
+
 indexes-of@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
@@ -4021,7 +4146,7 @@ is-retina@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/is-retina/-/is-retina-1.0.3.tgz#d7401b286bea2ae37f62477588de504d0b8647e3"
 
-is-stream@^1.0.1:
+is-stream@^1.0.1, is-stream@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
 
@@ -4297,6 +4422,13 @@ jest-matcher-utils@^18.1.0:
     chalk "^1.1.3"
     pretty-format "^18.1.0"
 
+jest-matcher-utils@^19.0.0:
+  version "19.0.0"
+  resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-19.0.0.tgz#5ecd9b63565d2b001f61fbf7ec4c7f537964564d"
+  dependencies:
+    chalk "^1.1.3"
+    pretty-format "^19.0.0"
+
 jest-matchers@^18.1.0:
   version "18.1.0"
   resolved "https://registry.yarnpkg.com/jest-matchers/-/jest-matchers-18.1.0.tgz#0341484bf87a1fd0bac0a4d2c899e2b77a3f1ead"
@@ -4368,6 +4500,15 @@ jest-util@^18.1.0:
     jest-mock "^18.0.0"
     mkdirp "^0.5.1"
 
+jest-validate@19.0.0:
+  version "19.0.0"
+  resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-19.0.0.tgz#8c6318a20ecfeaba0ba5378bfbb8277abded4173"
+  dependencies:
+    chalk "^1.1.1"
+    jest-matcher-utils "^19.0.0"
+    leven "^2.0.0"
+    pretty-format "^19.0.0"
+
 jest@^18.1.0:
   version "18.1.0"
   resolved "https://registry.yarnpkg.com/jest/-/jest-18.1.0.tgz#bcebf1e203dee5c2ad2091c805300a343d9e6c7d"
@@ -4639,6 +4780,10 @@ leaflet@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.0.3.tgz#1f401b98b45c8192134c6c8d69686253805007c8"
 
+leven@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
+
 levn@^0.3.0, levn@~0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
@@ -4646,6 +4791,65 @@ levn@^0.3.0, levn@~0.3.0:
     prelude-ls "~1.1.2"
     type-check "~0.3.2"
 
+lint-staged@^3.3.1:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-3.3.1.tgz#b725d98a2be1f82cb228069fab682f503c95234d"
+  dependencies:
+    app-root-path "^2.0.0"
+    cosmiconfig "^1.1.0"
+    execa "^0.6.0"
+    listr "^0.11.0"
+    minimatch "^3.0.0"
+    npm-which "^3.0.1"
+    staged-git-files "0.0.4"
+    which "^1.2.11"
+
+listr-silent-renderer@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e"
+
+listr-update-renderer@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.2.0.tgz#ca80e1779b4e70266807e8eed1ad6abe398550f9"
+  dependencies:
+    chalk "^1.1.3"
+    cli-truncate "^0.2.1"
+    elegant-spinner "^1.0.1"
+    figures "^1.7.0"
+    indent-string "^3.0.0"
+    log-symbols "^1.0.2"
+    log-update "^1.0.2"
+    strip-ansi "^3.0.1"
+
+listr-verbose-renderer@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.4.0.tgz#44dc01bb0c34a03c572154d4d08cde9b1dc5620f"
+  dependencies:
+    chalk "^1.1.3"
+    cli-cursor "^1.0.2"
+    date-fns "^1.27.2"
+    figures "^1.7.0"
+
+listr@^0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/listr/-/listr-0.11.0.tgz#5e778bc23806ac3ab984ed75564458151f39b03e"
+  dependencies:
+    chalk "^1.1.3"
+    cli-truncate "^0.2.1"
+    figures "^1.7.0"
+    indent-string "^2.1.0"
+    is-promise "^2.1.0"
+    is-stream "^1.1.0"
+    listr-silent-renderer "^1.1.1"
+    listr-update-renderer "^0.2.0"
+    listr-verbose-renderer "^0.4.0"
+    log-symbols "^1.0.2"
+    log-update "^1.0.2"
+    ora "^0.2.3"
+    rxjs "^5.0.0-beta.11"
+    stream-to-observable "^0.1.0"
+    strip-ansi "^3.0.1"
+
 load-json-file@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@@ -4883,6 +5087,19 @@ lodash@^3.7.0, lodash@^3.8.0:
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
 
+log-symbols@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
+  dependencies:
+    chalk "^1.0.0"
+
+log-update@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1"
+  dependencies:
+    ansi-escapes "^1.0.0"
+    cli-cursor "^1.0.2"
+
 log4js@^0.6.31:
   version "0.6.38"
   resolved "https://registry.yarnpkg.com/log4js/-/log4js-0.6.38.tgz#2c494116695d6fb25480943d3fc872e662a522fd"
@@ -4912,6 +5129,13 @@ lru-cache@^2.6.5:
   version "2.7.3"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952"
 
+lru-cache@^4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e"
+  dependencies:
+    pseudomap "^1.0.1"
+    yallist "^2.0.0"
+
 lru-queue@0.1:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"
@@ -5057,7 +5281,7 @@ minimist@0.0.8:
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
 
-minimist@^1.1.1, minimist@^1.2.0:
+minimist@1.2.0, minimist@>=0.2.0, minimist@^1.1.1, minimist@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
 
@@ -5279,6 +5503,10 @@ normalize-package-data@^2.3.2:
     semver "2 || 3 || 4 || 5"
     validate-npm-package-license "^3.0.1"
 
+normalize-path@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379"
+
 normalize-path@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a"
@@ -5300,6 +5528,26 @@ normalizr@^3.0.2:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/normalizr/-/normalizr-3.2.1.tgz#85a2d3d0ffb9c3b08f4131cb8d8fbfb7e9211b35"
 
+npm-path@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/npm-path/-/npm-path-2.0.3.tgz#15cff4e1c89a38da77f56f6055b24f975dfb2bbe"
+  dependencies:
+    which "^1.2.10"
+
+npm-run-path@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+  dependencies:
+    path-key "^2.0.0"
+
+npm-which@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/npm-which/-/npm-which-3.0.1.tgz#9225f26ec3a285c209cae67c3b11a6b4ab7140aa"
+  dependencies:
+    commander "^2.9.0"
+    npm-path "^2.0.2"
+    which "^1.2.10"
+
 npmlog@^4.0.1:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.0.2.tgz#d03950e0e78ce1527ba26d2a7592e9348ac3e75f"
@@ -5465,6 +5713,15 @@ options@>=0.0.5:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f"
 
+ora@^0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4"
+  dependencies:
+    chalk "^1.1.1"
+    cli-cursor "^1.0.2"
+    cli-spinners "^0.1.2"
+    object-assign "^4.0.1"
+
 original@>=0.0.5:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/original/-/original-1.0.0.tgz#9147f93fa1696d04be61e01bd50baeaca656bd3b"
@@ -5504,6 +5761,10 @@ output-file-sync@^1.1.0:
     mkdirp "^0.5.1"
     object-assign "^4.1.0"
 
+p-finally@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+
 pako@~0.2.0:
   version "0.2.9"
   resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
@@ -5589,6 +5850,10 @@ path-is-inside@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
 
+path-key@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+
 path-parse@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
@@ -6197,6 +6462,21 @@ preserve@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
 
+prettier@^0.21.0:
+  version "0.21.0"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-0.21.0.tgz#5187ab95fdd9ca63dccf6217ed03b434d72771f8"
+  dependencies:
+    ast-types "0.9.4"
+    babel-code-frame "6.22.0"
+    babylon "6.15.0"
+    chalk "1.1.3"
+    esutils "2.0.2"
+    flow-parser "0.40.0"
+    get-stdin "5.0.1"
+    glob "7.1.1"
+    jest-validate "19.0.0"
+    minimist "1.2.0"
+
 pretty-error@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.0.2.tgz#a7db19cbb529ca9f0af3d3a2f77d5caf8e5dec23"
@@ -6210,6 +6490,12 @@ pretty-format@^18.1.0:
   dependencies:
     ansi-styles "^2.2.1"
 
+pretty-format@^19.0.0:
+  version "19.0.0"
+  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-19.0.0.tgz#56530d32acb98a3fa4851c4e2b9d37b420684c84"
+  dependencies:
+    ansi-styles "^3.0.0"
+
 private@^0.1.6, private@~0.1.5:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
@@ -6257,6 +6543,10 @@ prr@~0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
 
+pseudomap@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+
 public-encrypt@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6"
@@ -6969,6 +7259,12 @@ rx@2.3.24:
   version "2.3.24"
   resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7"
 
+rxjs@^5.0.0-beta.11:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.2.0.tgz#db537de8767c05fa73721587a29e0085307d318b"
+  dependencies:
+    symbol-observable "^1.0.1"
+
 safe-buffer@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
@@ -7108,6 +7404,16 @@ shallowequal@0.2.x, shallowequal@^0.2.2:
   dependencies:
     lodash.keys "^3.1.2"
 
+shebang-command@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+  dependencies:
+    shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+
 shelljs@^0.7.4, shelljs@^0.7.5:
   version "0.7.6"
   resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.6.tgz#379cccfb56b91c8601e4793356eb5382924de9ad"
@@ -7300,6 +7606,10 @@ stack-trace@0.0.x:
   version "0.0.9"
   resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.9.tgz#a8f6eaeca90674c333e7c43953f275b451510695"
 
+staged-git-files@0.0.4:
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-0.0.4.tgz#d797e1b551ca7a639dec0237dc6eb4bb9be17d35"
+
 "statuses@>= 1.3.1 < 2", statuses@~1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
@@ -7325,6 +7635,10 @@ stream-http@^2.3.1:
     to-arraybuffer "^1.0.0"
     xtend "^4.0.0"
 
+stream-to-observable@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.1.0.tgz#45bf1d9f2d7dc09bed81f1c307c430e68b84cffe"
+
 strict-uri-encode@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
@@ -7394,6 +7708,10 @@ strip-bom@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
 
+strip-eof@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+
 strip-json-comments@~2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
@@ -7430,7 +7748,7 @@ svgo@^0.7.0:
     sax "~1.2.1"
     whet.extend "~0.9.9"
 
-symbol-observable@^1.0.2, symbol-observable@^1.0.4:
+symbol-observable@^1.0.1, symbol-observable@^1.0.2, symbol-observable@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d"
 
@@ -7980,7 +8298,7 @@ which-module@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
 
-which@^1.0.5, which@^1.1.1, which@^1.2.1:
+which@^1.0.5, which@^1.1.1, which@^1.2.1, which@^1.2.10, which@^1.2.11, which@^1.2.9:
   version "1.2.12"
   resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192"
   dependencies:
@@ -8107,6 +8425,10 @@ y18n@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
 
+yallist@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.0.0.tgz#306c543835f09ee1a4cb23b7bce9ab341c91cdd4"
+
 yargs-parser@^4.2.0:
   version "4.2.1"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c"