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/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 133fc5c27f05bcdf1b76aa478e0e8056e9cb8067..ea5abefc8c5f745bece440f429f9836f0d7dfd12 100644
--- a/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx
@@ -2,7 +2,7 @@ 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 cx from "classnames";
 
@@ -56,7 +56,7 @@ export default class PartialQueryBuilder extends Component {
                 }
             }
         };
-        let previewUrl = "/q#" + serializeCardForUrl(previewCard);
+        let previewUrl = Urls.question(null, previewCard);
 
         const onChange = (query) => {
             this.props.onChange(query);
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/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/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/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/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/lib/card.js b/frontend/src/metabase/lib/card.js
index b53d09d1732ae751d2dcddc8f34f73d8986ac573..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;
@@ -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/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/urls.js b/frontend/src/metabase/lib/urls.js
index ed11450f3b0fd4c68e1de077a1e29dc25ee491ea..7c2eba73166aeef3535ac9f4ee9e9f3b995fba8d 100644
--- a/frontend/src/metabase/lib/urls.js
+++ b/frontend/src/metabase/lib/urls.js
@@ -1,66 +1,71 @@
 import { serializeCardForUrl } from "metabase/lib/card";
 
 // provides functions for building urls to things we care about
-var Urls = {
-    q: function(card) {
-        return "/q#" + serializeCardForUrl(card);
-    },
 
-    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/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/pulse/components/PulseEditChannels.jsx b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
index 5029bf3945bc720c1d0b881a2f4918386d0ffc76..c92a3215312688b996aea6152064d99f87ea7cc5 100644
--- a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
@@ -152,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}
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/query_builder/components/QueryDownloadWidget.jsx b/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx
index 58ead4423d3d5bfc1e7931c0bb4b6f11bb1d2664..337b5ac6b1f0c91c5523244ba746833e0b63483e 100644
--- a/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx
@@ -7,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";
diff --git a/frontend/src/metabase/query_builder/components/QueryHeader.jsx b/frontend/src/metabase/query_builder/components/QueryHeader.jsx
index 2e61da66556b74a6600bced942a4b3457a78ee0d..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";
diff --git a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
index 0ed51d41d15292d96d9c9e0413c6ba4192782011..a32d4ccc783c6592a8a8e3fddc7259554436f3e9 100644
--- a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
@@ -19,6 +19,7 @@ import QuestionEmbedWidget from "../containers/QuestionEmbedWidget";
 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";
@@ -223,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/VisualizationResult.jsx b/frontend/src/metabase/query_builder/components/VisualizationResult.jsx
index 3a6c8a94d96f2c99336e86039b8ba1d8bab8e29a..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);
+
     if (isObjectDetail) {
         return <QueryVisualizationObjectDetailTable data={result.data} {...props} />
-    } else if (result.data.rows.length === 0) {
+    } else if (noResults) {
         // successful query but there were 0 rows returned with the result
         return <VisualizationErrorMessage
                   type='noRows'
diff --git a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
index 95f367df9fa450f25cb94ddc88ed670c35a0138b..e0fddf9f11224e94198af92083e27dd7dae97aee 100644
--- a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
+++ b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
@@ -154,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);
         }
     }
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/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/containers/ReferenceEntityList.jsx b/frontend/src/metabase/reference/containers/ReferenceEntityList.jsx
index daf3818bd173b97955d390ef587e863b4299d9a7..9f821341533866af791d903eec787c720cec6106 100644
--- a/frontend/src/metabase/reference/containers/ReferenceEntityList.jsx
+++ b/frontend/src/metabase/reference/containers/ReferenceEntityList.jsx
@@ -5,6 +5,7 @@ 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";
@@ -57,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/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.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/visualizations/components/LegendHeader.jsx b/frontend/src/metabase/visualizations/components/LegendHeader.jsx
index 0d29843b595d4362aef8c5523adc295b86978fbe..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";
 
@@ -65,7 +65,7 @@ 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}
diff --git a/frontend/src/metabase/visualizations/components/Visualization.jsx b/frontend/src/metabase/visualizations/components/Visualization.jsx
index 404daf44cd669f772dc44af176485e21fee053fe..29d0a5eb9aa79a891983f7b39cec3288c2768541 100644
--- a/frontend/src/metabase/visualizations/components/Visualization.jsx
+++ b/frontend/src/metabase/visualizations/components/Visualization.jsx
@@ -15,13 +15,14 @@ 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 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";
 
@@ -273,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 = (
diff --git a/frontend/src/metabase/visualizations/visualizations/PieChart.jsx b/frontend/src/metabase/visualizations/visualizations/PieChart.jsx
index b922e95cb76714c4bd966056a2e65ff4b5922392..1e4420e735f70313604df9b09944eda2e0680887 100644
--- a/frontend/src/metabase/visualizations/visualizations/PieChart.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/PieChart.jsx
@@ -184,8 +184,8 @@ export default class PieChart extends Component<*, Props, *> {
 
         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);
diff --git a/frontend/src/metabase/visualizations/visualizations/Scalar.jsx b/frontend/src/metabase/visualizations/visualizations/Scalar.jsx
index 33dda37c911ae1e6c651788909cefd9e5ad26151..0109394d22e216b61bba2ae5395b0ce4892f1645 100644
--- a/frontend/src/metabase/visualizations/visualizations/Scalar.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Scalar.jsx
@@ -8,7 +8,7 @@ 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";
@@ -195,7 +195,7 @@ export default class Scalar extends Component<*, VisualizationProps, *> {
                 <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/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/package.json b/package.json
index 8cebcc3d1696c7a4c1b6cf3a107c98c47033f1d4..77edb5a62183b55dc27d90deac3fb94b2cfdd11f 100644
--- a/package.json
+++ b/package.json
@@ -102,6 +102,7 @@
     "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",
@@ -153,7 +154,7 @@
     "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",
diff --git a/project.clj b/project.clj
index 2dd29bb69d5e83eaf740572b4433f1cc6093e73b..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,7 +25,7 @@
                   :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.5.0"]                           ; JSON Web Tokens; High-Level message signing library
                  [cheshire "5.7.0"]                                   ; fast JSON encoding (used by Ring JSON middleware)
@@ -43,10 +43,10 @@
                                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
@@ -68,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"}
@@ -107,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/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/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/generic_sql.clj b/src/metabase/driver/generic_sql.clj
index 4c1364d7ba01a6182f3faa329a72342280d94834..dbec2699e8e5f9c135fab69c7843b3bfcb5edf3b 100644
--- a/src/metabase/driver/generic_sql.clj
+++ b/src/metabase/driver/generic_sql.clj
@@ -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
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/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/query_processor/sql_parameters.clj b/src/metabase/query_processor/sql_parameters.clj
index 42e2b6d698a8942b6273bc5187d5eed586cee3fc..a48cb7b71d810e10af732b03f382f4a5c1462b52 100644
--- a/src/metabase/query_processor/sql_parameters.clj
+++ b/src/metabase/query_processor/sql_parameters.clj
@@ -243,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]}]
@@ -345,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/sync_database/analyze.clj b/src/metabase/sync_database/analyze.clj
index c21efd882d27f4931166737435daaf35cc22655a..bad37a0c7a53b7e267668e923e7fc08daa921f1b 100644
--- a/src/metabase/sync_database/analyze.clj
+++ b/src/metabase/sync_database/analyze.clj
@@ -186,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/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/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/query_processor/sql_parameters_test.clj b/test/metabase/query_processor/sql_parameters_test.clj
index 44585c279c3a8bc65e8cdd29c943f5bf1c0a2c46..dda33ecf8b2442c1e0c354b4914fd7f1595774aa 100644
--- a/test/metabase/query_processor/sql_parameters_test.clj
+++ b/test/metabase/query_processor/sql_parameters_test.clj
@@ -428,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) \.))
@@ -527,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_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/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/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 65023c03997031bf46ff207d48f499d916c07e1d..a343edb47001c17cfd759be5ee61eccd6f0f3c6e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3723,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"